SystemUI Split via TaskOrganizer

Use the early TaskOrganizer concepts to implement Split-screen
in system-ui.

This includes changes to both FW and SystemUI. The changes to
FW involve removing the use of split-screen specific behavior (like
minimize dock and direct ordering) and also reducing things that
care about primary vs secondary. It also changed ActivityStack
to inherit bounds from parent** when in split-mode so that sysui
only needs to manipulate the tile and/or reparent stacks to
effect their geometry.

This means a lot of layout logic moves to SystemUI. The bulk of
the work done in ActivityStack which is split-screen related is
moved into SplitDisplayLayout. This basically takes a snapshot of
display configuration and manages the sizes of splits and their
snap targets.

Intermediate dragging of divider bar now only moves root task leashes
around rather than talking to WM. This includes position as well
as crop (which used to be stack crop). Once the user releases
the divider bar, it will calculate (based on snaps) the new
root task sizes and update their configurations via
WindowContainerTransaction. Because the interim updates are only
on the leashes, no configuration updates occur until the end.

Entering/Exiting split-mode is now handled by SplitScreentaskOrganizer#
onTaskInfoChanged. This is effectively a state-machine that
looks at the current split task membership vs. previous and then decides
when to move things into/out-of split tasks and how to coordinate with the
DividerView.

Minimized dock is relegated to a purely system-ui concept. To
accomplish this, **the home *stack* is set to the minimizedhomebounds
by systemui. This means that it's relative position to its parent is
negative! This allows us to leave the split sizes constant, have
their children inherit the "actual" split sizes, but keep the
home stack unchanging in its minimized size. We just adjust the crop
negative to reveal it.

IME handling is done through the same mechanism as app-driven IME
animation... only Divider receives the control instead of the app.
This allows synchronized animation of split tasks with IME. To
account for insets, though, when IME is opened, the bottom stack
will be repositioned in WM.

Bug: 133381284
Test: Manual, use split-screen, rotate device, launch unresizable
      apps in split, use divider snap to close/maximize apps, etc.
Change-Id: I7133e151a1037c42b275b97857936437a7a6725f
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index 2d288ff..b1d39f5 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -123,4 +123,9 @@
      */
      void handleImageAsScreenshot(in Bitmap screenImage, in Rect locationInScreen,
               in Insets visibleInsets, int taskId) = 21;
+
+    /**
+     * Sets the split-screen divider minimized state
+     */
+    void setSplitScreenMinimized(boolean minimized) = 22;
 }
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index d317b7e..69bc259 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -60,6 +60,7 @@
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
 import com.android.systemui.shared.system.PackageManagerWrapper;
+import com.android.systemui.stackdivider.Divider;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NavigationBarController;
 import com.android.systemui.statusbar.NotificationListener;
@@ -327,6 +328,7 @@
     @Inject Lazy<DisplayImeController> mDisplayImeController;
     @Inject Lazy<RecordingController> mRecordingController;
     @Inject Lazy<ProtoTracer> mProtoTracer;
+    @Inject Lazy<Divider> mDivider;
 
     @Inject
     public Dependency() {
@@ -527,6 +529,7 @@
         mProviders.put(AutoHideController.class, mAutoHideController::get);
 
         mProviders.put(RecordingController.class, mRecordingController::get);
+        mProviders.put(Divider.class, mDivider::get);
 
         sDependency = this;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/DockedStackExistsListener.java b/packages/SystemUI/src/com/android/systemui/DockedStackExistsListener.java
deleted file mode 100644
index 5c0df17..0000000
--- a/packages/SystemUI/src/com/android/systemui/DockedStackExistsListener.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2017 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.systemui;
-
-import android.os.RemoteException;
-import android.util.Log;
-import android.view.IDockedStackListener;
-import android.view.WindowManagerGlobal;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.function.Consumer;
-
-/**
- * Utility wrapper to listen for whether or not a docked stack exists, to be
- * used for things like the different overview icon in that mode.
- */
-public class DockedStackExistsListener {
-
-    private static final String TAG = "DockedStackExistsListener";
-
-    private static ArrayList<WeakReference<Consumer<Boolean>>> sCallbacks = new ArrayList<>();
-    private static boolean mLastExists;
-
-    static {
-        try {
-            WindowManagerGlobal.getWindowManagerService().registerDockedStackListener(
-                    new IDockedStackListener.Stub() {
-                        @Override
-                        public void onDividerVisibilityChanged(boolean b) throws RemoteException {
-
-                        }
-
-                        @Override
-                        public void onDockedStackExistsChanged(boolean exists)
-                                throws RemoteException {
-                            DockedStackExistsListener.onDockedStackExistsChanged(exists);
-                        }
-
-                        @Override
-                        public void onDockedStackMinimizedChanged(boolean b, long l, boolean b1)
-                                throws RemoteException {
-
-                        }
-
-                        @Override
-                        public void onAdjustedForImeChanged(boolean b, long l)
-                                throws RemoteException {
-
-                        }
-
-                        @Override
-                        public void onDockSideChanged(int i) throws RemoteException {
-
-                        }
-                    });
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failed registering docked stack exists listener", e);
-        }
-    }
-
-
-    private static void onDockedStackExistsChanged(boolean exists) {
-        mLastExists = exists;
-        synchronized (sCallbacks) {
-            sCallbacks.removeIf(wf -> {
-                Consumer<Boolean> l = wf.get();
-                if (l != null) l.accept(exists);
-                return l == null;
-            });
-        }
-    }
-
-    public static void register(Consumer<Boolean> callback) {
-        callback.accept(mLastExists);
-        synchronized (sCallbacks) {
-            sCallbacks.add(new WeakReference<>(callback));
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/TransactionPool.java b/packages/SystemUI/src/com/android/systemui/TransactionPool.java
new file mode 100644
index 0000000..801cf8a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/TransactionPool.java
@@ -0,0 +1,55 @@
+/*
+ * 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.systemui;
+
+import android.util.Pools;
+import android.view.SurfaceControl;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Provides a synchronized pool of {@link SurfaceControl.Transaction}s to minimize allocations.
+ */
+@Singleton
+public class TransactionPool {
+    private final Pools.SynchronizedPool<SurfaceControl.Transaction> mTransactionPool =
+            new Pools.SynchronizedPool<>(4);
+
+    @Inject
+    TransactionPool() {
+    }
+
+    /** Gets a transaction from the pool. */
+    public SurfaceControl.Transaction acquire() {
+        SurfaceControl.Transaction t = mTransactionPool.acquire();
+        if (t == null) {
+            return new SurfaceControl.Transaction();
+        }
+        return t;
+    }
+
+    /**
+     * Return a transaction to the pool. DO NOT call {@link SurfaceControl.Transaction#close()} if
+     * returning to pool.
+     */
+    public void release(SurfaceControl.Transaction t) {
+        if (!mTransactionPool.release(t)) {
+            t.close();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index ad49364..34cad51 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -380,6 +380,14 @@
                     taskId, mHandler, null);
         }
 
+        @Override
+        public void setSplitScreenMinimized(boolean minimized) {
+            Divider divider = mDividerOptional.get();
+            if (divider != null) {
+                divider.setMinimized(minimized);
+            }
+        }
+
         private boolean verifyCaller(String reason) {
             final int callerId = Binder.getCallingUserHandle().getIdentifier();
             if (callerId != mCurrentBoundedUserId) {
diff --git a/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java b/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java
index 5ae0954..4f20492 100644
--- a/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java
+++ b/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java
@@ -22,10 +22,8 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.os.RemoteException;
-import android.util.Log;
 import android.view.IWindowManager;
 import android.view.KeyEvent;
-import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 
 import com.android.internal.policy.DividerSnapAlgorithm;
@@ -94,29 +92,24 @@
     }
 
     private void handleDockKey(long shortcutCode) {
-        try {
-            int dockSide = mWindowManagerService.getDockedStackSide();
-            if (dockSide == WindowManager.DOCKED_INVALID) {
-                // Split the screen
-                mRecents.splitPrimaryTask((shortcutCode == SC_DOCK_LEFT)
-                        ? SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT
-                        : SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT, null, -1);
-            } else {
-                // If there is already a docked window, we respond by resizing the docking pane.
-                DividerView dividerView = mDivider.getView();
-                DividerSnapAlgorithm snapAlgorithm = dividerView.getSnapAlgorithm();
-                int dividerPosition = dividerView.getCurrentPosition();
-                DividerSnapAlgorithm.SnapTarget currentTarget =
-                        snapAlgorithm.calculateNonDismissingSnapTarget(dividerPosition);
-                DividerSnapAlgorithm.SnapTarget target = (shortcutCode == SC_DOCK_LEFT)
-                        ? snapAlgorithm.getPreviousTarget(currentTarget)
-                        : snapAlgorithm.getNextTarget(currentTarget);
-                dividerView.startDragging(true /* animate */, false /* touching */);
-                dividerView.stopDragging(target.position, 0f, false /* avoidDismissStart */,
-                        true /* logMetrics */);
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "handleDockKey() failed.");
+        if (mDivider == null || !mDivider.inSplitMode()) {
+            // Split the screen
+            mRecents.splitPrimaryTask((shortcutCode == SC_DOCK_LEFT)
+                    ? SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT
+                    : SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT, null, -1);
+        } else {
+            // If there is already a docked window, we respond by resizing the docking pane.
+            DividerView dividerView = mDivider.getView();
+            DividerSnapAlgorithm snapAlgorithm = dividerView.getSnapAlgorithm();
+            int dividerPosition = dividerView.getCurrentPosition();
+            DividerSnapAlgorithm.SnapTarget currentTarget =
+                    snapAlgorithm.calculateNonDismissingSnapTarget(dividerPosition);
+            DividerSnapAlgorithm.SnapTarget target = (shortcutCode == SC_DOCK_LEFT)
+                    ? snapAlgorithm.getPreviousTarget(currentTarget)
+                    : snapAlgorithm.getNextTarget(currentTarget);
+            dividerView.startDragging(true /* animate */, false /* touching */);
+            dividerView.stopDragging(target.position, 0f, false /* avoidDismissStart */,
+                    true /* logMetrics */);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java
index 90cc0e57..2daefbd 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java
@@ -17,69 +17,283 @@
 package com.android.systemui.stackdivider;
 
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.Display.DEFAULT_DISPLAY;
 
+import android.app.ActivityTaskManager;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Handler;
 import android.os.RemoteException;
+import android.provider.Settings;
 import android.util.Log;
-import android.view.IDockedStackListener;
+import android.util.Slog;
+import android.view.IWindowContainer;
 import android.view.LayoutInflater;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
 import android.view.View;
-import android.view.WindowManagerGlobal;
+import android.view.WindowContainerTransaction;
 
+import com.android.internal.policy.DividerSnapAlgorithm;
 import com.android.systemui.R;
 import com.android.systemui.SystemUI;
+import com.android.systemui.TransactionPool;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.wm.DisplayChangeController;
+import com.android.systemui.wm.DisplayController;
+import com.android.systemui.wm.DisplayImeController;
+import com.android.systemui.wm.DisplayLayout;
+import com.android.systemui.wm.SystemWindows;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
 import java.util.Optional;
+import java.util.function.Consumer;
+
+import javax.inject.Singleton;
 
 import dagger.Lazy;
 
 /**
  * Controls the docked stack divider.
  */
-public class Divider extends SystemUI implements DividerView.DividerCallbacks {
+@Singleton
+public class Divider extends SystemUI implements DividerView.DividerCallbacks,
+        DisplayController.OnDisplaysChangedListener {
     private static final String TAG = "Divider";
+
+    static final boolean DEBUG = true;
+
+    static final int DEFAULT_APP_TRANSITION_DURATION = 336;
+
     private final Optional<Lazy<Recents>> mRecentsOptionalLazy;
 
     private DividerWindowManager mWindowManager;
     private DividerView mView;
     private final DividerState mDividerState = new DividerState();
-    private DockDividerVisibilityListener mDockDividerVisibilityListener;
     private boolean mVisible = false;
     private boolean mMinimized = false;
     private boolean mAdjustedForIme = false;
     private boolean mHomeStackResizable = false;
     private ForcedResizableInfoActivityController mForcedResizableController;
+    private SystemWindows mSystemWindows;
+    final SurfaceSession mSurfaceSession = new SurfaceSession();
+    private DisplayController mDisplayController;
+    private DisplayImeController mImeController;
+    final TransactionPool mTransactionPool;
 
-    public Divider(Context context, Optional<Lazy<Recents>> recentsOptionalLazy) {
+    // Keeps track of real-time split geometry including snap positions and ime adjustments
+    private SplitDisplayLayout mSplitLayout;
+
+    // Transient: this contains the layout calculated for a new rotation requested by WM. This is
+    // kept around so that we can wait for a matching configuration change and then use the exact
+    // layout that we sent back to WM.
+    private SplitDisplayLayout mRotateSplitLayout;
+
+    private Handler mHandler;
+    private KeyguardStateController mKeyguardStateController;
+
+    private final ArrayList<WeakReference<Consumer<Boolean>>> mDockedStackExistsListeners =
+            new ArrayList<>();
+
+    private SplitScreenTaskOrganizer mSplits = new SplitScreenTaskOrganizer(this);
+
+    private DisplayChangeController.OnDisplayChangingListener mRotationController =
+            (display, fromRotation, toRotation, t) -> {
+                DisplayLayout displayLayout =
+                        new DisplayLayout(mDisplayController.getDisplayLayout(display));
+                SplitDisplayLayout sdl = new SplitDisplayLayout(mContext, displayLayout, mSplits);
+                sdl.rotateTo(toRotation);
+                mRotateSplitLayout = sdl;
+                int position = mMinimized ? mView.mSnapTargetBeforeMinimized.position
+                        : mView.getCurrentPosition();
+                DividerSnapAlgorithm snap = sdl.getSnapAlgorithm();
+                final DividerSnapAlgorithm.SnapTarget target =
+                        snap.calculateNonDismissingSnapTarget(position);
+                sdl.resizeSplits(target.position, t);
+
+                if (inSplitMode()) {
+                    WindowManagerProxy.applyHomeTasksMinimized(sdl, mSplits.mSecondary.token, t);
+                }
+            };
+
+    private IWindowContainer mLastImeTarget = null;
+    private boolean mShouldAdjustForIme = false;
+
+    private DisplayImeController.ImePositionProcessor mImePositionProcessor =
+            new DisplayImeController.ImePositionProcessor() {
+                private int mStartTop = 0;
+                private int mFinalTop = 0;
+                @Override
+                public void onImeStartPositioning(int displayId, int imeTop, int finalImeTop,
+                        boolean showing, SurfaceControl.Transaction t) {
+                    mStartTop = imeTop;
+                    mFinalTop = finalImeTop;
+                    if (showing) {
+                        try {
+                            mLastImeTarget = ActivityTaskManager.getTaskOrganizerController()
+                                    .getImeTarget(displayId);
+                            mShouldAdjustForIme = !mSplitLayout.mDisplayLayout.isLandscape()
+                                    && (mLastImeTarget.asBinder()
+                                    == mSplits.mSecondary.token.asBinder());
+                        } catch (RemoteException e) {
+                            Slog.w(TAG, "Failed to get IME target", e);
+                        }
+                    }
+                    if (!mShouldAdjustForIme) {
+                        setAdjustedForIme(false);
+                        return;
+                    }
+                    mView.setAdjustedForIme(showing, showing
+                            ? DisplayImeController.ANIMATION_DURATION_SHOW_MS
+                            : DisplayImeController.ANIMATION_DURATION_HIDE_MS);
+                    // Reposition the server's secondary split position so that it evaluates
+                    // insets properly.
+                    WindowContainerTransaction wct = new WindowContainerTransaction();
+                    if (showing) {
+                        mSplitLayout.updateAdjustedBounds(finalImeTop, imeTop, finalImeTop);
+                        wct.setBounds(mSplits.mSecondary.token, mSplitLayout.mAdjustedSecondary);
+                    } else {
+                        wct.setBounds(mSplits.mSecondary.token, mSplitLayout.mSecondary);
+                    }
+                    try {
+                        ActivityTaskManager.getTaskOrganizerController()
+                                .applyContainerTransaction(wct, null /* organizer */);
+                    } catch (RemoteException e) {
+                    }
+                    setAdjustedForIme(showing);
+                }
+
+                @Override
+                public void onImePositionChanged(int displayId, int imeTop,
+                        SurfaceControl.Transaction t) {
+                    if (!mShouldAdjustForIme) {
+                        return;
+                    }
+                    mSplitLayout.updateAdjustedBounds(imeTop, mStartTop, mFinalTop);
+                    mView.resizeSplitSurfaces(t, mSplitLayout.mAdjustedPrimary,
+                            mSplitLayout.mAdjustedSecondary);
+                    final boolean showing = mFinalTop < mStartTop;
+                    final float progress = ((float) (imeTop - mStartTop)) / (mFinalTop - mStartTop);
+                    final float fraction = showing ? progress : 1.f - progress;
+                    mView.setResizeDimLayer(t, true /* primary */, fraction * 0.3f);
+                }
+
+                @Override
+                public void onImeEndPositioning(int displayId, int imeTop,
+                        boolean showing, SurfaceControl.Transaction t) {
+                    if (!mShouldAdjustForIme) {
+                        return;
+                    }
+                    mSplitLayout.updateAdjustedBounds(imeTop, mStartTop, mFinalTop);
+                    mView.resizeSplitSurfaces(t, mSplitLayout.mAdjustedPrimary,
+                            mSplitLayout.mAdjustedSecondary);
+                    mView.setResizeDimLayer(t, true /* primary */, showing ? 0.3f : 0.f);
+                }
+            };
+
+    public Divider(Context context, Optional<Lazy<Recents>> recentsOptionalLazy,
+            DisplayController displayController, SystemWindows systemWindows,
+            DisplayImeController imeController, Handler handler,
+            KeyguardStateController keyguardStateController, TransactionPool transactionPool) {
         super(context);
+        mDisplayController = displayController;
+        mSystemWindows = systemWindows;
+        mImeController = imeController;
+        mHandler = handler;
+        mKeyguardStateController = keyguardStateController;
         mRecentsOptionalLazy = recentsOptionalLazy;
+        mForcedResizableController = new ForcedResizableInfoActivityController(context, this);
+        mTransactionPool = transactionPool;
     }
 
     @Override
     public void start() {
-        mWindowManager = new DividerWindowManager(mContext);
-        update(mContext.getResources().getConfiguration());
-        mDockDividerVisibilityListener = new DockDividerVisibilityListener();
-        try {
-            WindowManagerGlobal.getWindowManagerService().registerDockedStackListener(
-                    mDockDividerVisibilityListener);
-        } catch (Exception e) {
-            Log.e(TAG, "Failed to register docked stack listener", e);
-        }
-        mForcedResizableController = new ForcedResizableInfoActivityController(mContext);
+        mWindowManager = new DividerWindowManager(mSystemWindows);
+        mDisplayController.addDisplayWindowListener(this);
+        // Hide the divider when keyguard is showing. Even though keyguard/statusbar is above
+        // everything, it is actually transparent except for notifications, so we still need to
+        // hide any surfaces that are below it.
+        // TODO(b/148906453): Figure out keyguard dismiss animation for divider view.
+        mKeyguardStateController.addCallback(new KeyguardStateController.Callback() {
+            @Override
+            public void onUnlockedChanged() {
+
+            }
+
+            @Override
+            public void onKeyguardShowingChanged() {
+                if (!inSplitMode() || mView == null) {
+                    return;
+                }
+                mView.setHidden(mKeyguardStateController.isShowing());
+            }
+
+            @Override
+            public void onKeyguardFadingAwayChanged() {
+
+            }
+        });
+        // Don't initialize the divider or anything until we get the default display.
     }
 
     @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
+    public void onDisplayAdded(int displayId) {
+        if (displayId != DEFAULT_DISPLAY) {
+            return;
+        }
+        mSplitLayout = new SplitDisplayLayout(mDisplayController.getDisplayContext(displayId),
+                mDisplayController.getDisplayLayout(displayId), mSplits);
+        mImeController.addPositionProcessor(mImePositionProcessor);
+        mDisplayController.addDisplayChangingController(mRotationController);
+        try {
+            mSplits.init(ActivityTaskManager.getTaskOrganizerController(), mSurfaceSession);
+            // Set starting tile bounds based on middle target
+            final WindowContainerTransaction tct = new WindowContainerTransaction();
+            int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
+            mSplitLayout.resizeSplits(midPos, tct);
+            ActivityTaskManager.getTaskOrganizerController().applyContainerTransaction(tct,
+                    null /* organizer */);
+        } catch (Exception e) {
+            Slog.e(TAG, "Failed to register docked stack listener", e);
+        }
+        update(mDisplayController.getDisplayContext(displayId).getResources().getConfiguration());
+    }
+
+    @Override
+    public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
+        if (displayId != DEFAULT_DISPLAY) {
+            return;
+        }
+        mSplitLayout = new SplitDisplayLayout(mDisplayController.getDisplayContext(displayId),
+                mDisplayController.getDisplayLayout(displayId), mSplits);
+        if (mRotateSplitLayout == null) {
+            int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
+            final WindowContainerTransaction tct = new WindowContainerTransaction();
+            mSplitLayout.resizeSplits(midPos, tct);
+            try {
+                ActivityTaskManager.getTaskOrganizerController().applyContainerTransaction(tct,
+                        null /* organizer */);
+            } catch (RemoteException e) {
+            }
+        } else if (mRotateSplitLayout != null
+                && mSplitLayout.mDisplayLayout.rotation()
+                        == mRotateSplitLayout.mDisplayLayout.rotation()) {
+            mSplitLayout.mPrimary = new Rect(mRotateSplitLayout.mPrimary);
+            mSplitLayout.mSecondary = new Rect(mRotateSplitLayout.mSecondary);
+            mRotateSplitLayout = null;
+        }
         update(newConfig);
     }
 
+    Handler getHandler() {
+        return mHandler;
+    }
+
     public DividerView getView() {
         return mView;
     }
@@ -92,18 +306,25 @@
         return mHomeStackResizable;
     }
 
+    /** {@code true} if this is visible */
+    public boolean inSplitMode() {
+        return mView != null && mView.getVisibility() == View.VISIBLE;
+    }
+
     private void addDivider(Configuration configuration) {
+        Context dctx = mDisplayController.getDisplayContext(mContext.getDisplayId());
         mView = (DividerView)
-                LayoutInflater.from(mContext).inflate(R.layout.docked_stack_divider, null);
-        mView.injectDependencies(mWindowManager, mDividerState, this);
+                LayoutInflater.from(dctx).inflate(R.layout.docked_stack_divider, null);
+        DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mContext.getDisplayId());
+        mView.injectDependencies(mWindowManager, mDividerState, this, mSplits, mSplitLayout);
         mView.setVisibility(mVisible ? View.VISIBLE : View.INVISIBLE);
         mView.setMinimizedDockStack(mMinimized, mHomeStackResizable);
-        final int size = mContext.getResources().getDimensionPixelSize(
+        final int size = dctx.getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.docked_stack_divider_thickness);
         final boolean landscape = configuration.orientation == ORIENTATION_LANDSCAPE;
-        final int width = landscape ? size : MATCH_PARENT;
-        final int height = landscape ? MATCH_PARENT : size;
-        mWindowManager.add(mView, width, height);
+        final int width = landscape ? size : displayLayout.width();
+        final int height = landscape ? displayLayout.height() : size;
+        mWindowManager.add(mView, width, height, mContext.getDisplayId());
     }
 
     private void removeDivider() {
@@ -116,65 +337,86 @@
     private void update(Configuration configuration) {
         removeDivider();
         addDivider(configuration);
-        if (mMinimized) {
+        if (mMinimized && mView != null) {
             mView.setMinimizedDockStack(true, mHomeStackResizable);
             updateTouchable();
         }
     }
 
-    private void updateVisibility(final boolean visible) {
-        mView.post(new Runnable() {
-            @Override
-            public void run() {
-                if (mVisible != visible) {
-                    mVisible = visible;
-                    mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+    void updateVisibility(final boolean visible) {
+        if (mVisible != visible) {
+            mVisible = visible;
+            mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
 
-                    // Update state because animations won't finish.
-                    mView.setMinimizedDockStack(mMinimized, mHomeStackResizable);
-                }
+            if (visible) {
+                mView.enterSplitMode(mHomeStackResizable);
+                // Update state because animations won't finish.
+                mView.setMinimizedDockStack(mMinimized, mHomeStackResizable);
+            } else {
+                mView.exitSplitMode();
+                // un-minimize so that next entry triggers minimize anim.
+                mView.setMinimizedDockStack(false /* minimized */, mHomeStackResizable);
             }
-        });
+            // Notify existence listeners
+            synchronized (mDockedStackExistsListeners) {
+                mDockedStackExistsListeners.removeIf(wf -> {
+                    Consumer<Boolean> l = wf.get();
+                    if (l != null) l.accept(visible);
+                    return l == null;
+                });
+            }
+        }
+    }
+
+    private void setHomeStackResizable(boolean resizable) {
+        if (mHomeStackResizable == resizable) {
+            return;
+        }
+        mHomeStackResizable = resizable;
+        if (!inSplitMode()) {
+            return;
+        }
+        WindowManagerProxy.applyHomeTasksMinimized(mSplitLayout, mSplits.mSecondary.token);
     }
 
     private void updateMinimizedDockedStack(final boolean minimized, final long animDuration,
             final boolean isHomeStackResizable) {
-        mView.post(new Runnable() {
-            @Override
-            public void run() {
-                mHomeStackResizable = isHomeStackResizable;
-                if (mMinimized != minimized) {
-                    mMinimized = minimized;
-                    updateTouchable();
-                    if (animDuration > 0) {
-                        mView.setMinimizedDockStack(minimized, animDuration, isHomeStackResizable);
-                    } else {
-                        mView.setMinimizedDockStack(minimized, isHomeStackResizable);
-                    }
-                }
+        setHomeStackResizable(isHomeStackResizable);
+        if (animDuration > 0) {
+            mView.setMinimizedDockStack(minimized, animDuration, isHomeStackResizable);
+        } else {
+            mView.setMinimizedDockStack(minimized, isHomeStackResizable);
+        }
+        updateTouchable();
+    }
+
+    /** Switch to minimized state if appropriate */
+    public void setMinimized(final boolean minimized) {
+        mHandler.post(() -> {
+            if (!inSplitMode()) {
+                return;
             }
+            if (mMinimized == minimized) {
+                return;
+            }
+            mMinimized = minimized;
+            mView.setMinimizedDockStack(minimized, getAnimDuration(), mHomeStackResizable);
+            updateTouchable();
         });
     }
 
-    private void notifyDockedStackExistsChanged(final boolean exists) {
-        mView.post(new Runnable() {
-            @Override
-            public void run() {
-                mForcedResizableController.notifyDockedStackExistsChanged(exists);
-            }
-        });
+    void setAdjustedForIme(boolean adjustedForIme) {
+        if (mAdjustedForIme == adjustedForIme) {
+            return;
+        }
+        mAdjustedForIme = adjustedForIme;
+        updateTouchable();
     }
 
     private void updateTouchable() {
         mWindowManager.setTouchable((mHomeStackResizable || !mMinimized) && !mAdjustedForIme);
     }
 
-    public void onRecentsActivityStarting() {
-        if (mView != null) {
-            mView.onRecentsActivityStarting();
-        }
-    }
-
     /**
      * Workaround for b/62528361, at the time recents has drawn, it may happen before a
      * configuration change to the Divider, and internally, the event will be posted to the
@@ -206,6 +448,9 @@
     }
 
     public void onAppTransitionFinished() {
+        if (mView == null) {
+            return;
+        }
         mForcedResizableController.onAppTransitionFinished();
     }
 
@@ -231,46 +476,66 @@
         pw.print("  mAdjustedForIme="); pw.println(mAdjustedForIme);
     }
 
-    class DockDividerVisibilityListener extends IDockedStackListener.Stub {
+    long getAnimDuration() {
+        float transitionScale = Settings.Global.getFloat(mContext.getContentResolver(),
+                Settings.Global.TRANSITION_ANIMATION_SCALE,
+                mContext.getResources().getFloat(
+                        com.android.internal.R.dimen
+                                .config_appTransitionAnimationDurationScaleDefault));
+        final long transitionDuration = DEFAULT_APP_TRANSITION_DURATION;
+        return (long) (transitionDuration * transitionScale);
+    }
 
-        @Override
-        public void onDividerVisibilityChanged(boolean visible) throws RemoteException {
-            updateVisibility(visible);
+    /** Register a listener that gets called whenever the existence of the divider changes */
+    public void registerInSplitScreenListener(Consumer<Boolean> listener) {
+        listener.accept(inSplitMode());
+        synchronized (mDockedStackExistsListeners) {
+            mDockedStackExistsListeners.add(new WeakReference<>(listener));
         }
+    }
 
-        @Override
-        public void onDockedStackExistsChanged(boolean exists) throws RemoteException {
-            notifyDockedStackExistsChanged(exists);
+    void startEnterSplit() {
+        // Set resizable directly here because applyEnterSplit already resizes home stack.
+        mHomeStackResizable = WindowManagerProxy.applyEnterSplit(mSplits, mSplitLayout);
+    }
+
+    void ensureMinimizedSplit() {
+        final boolean wasMinimized = mMinimized;
+        mMinimized = true;
+        setHomeStackResizable(mSplits.mSecondary.isResizable());
+        if (!inSplitMode()) {
+            // Wasn't in split-mode yet, so enter now.
+            if (DEBUG) {
+                Log.d(TAG, " entering split mode with minimized=true");
+            }
+            updateVisibility(true /* visible */);
+        } else if (!wasMinimized) {
+            if (DEBUG) {
+                Log.d(TAG, " in split mode, but minimizing ");
+            }
+            // Was already in split-mode, update just minimized state.
+            updateMinimizedDockedStack(mMinimized, getAnimDuration(),
+                    mHomeStackResizable);
         }
+    }
 
-        @Override
-        public void onDockedStackMinimizedChanged(boolean minimized, long animDuration,
-                boolean isHomeStackResizable) throws RemoteException {
-            mHomeStackResizable = isHomeStackResizable;
-            updateMinimizedDockedStack(minimized, animDuration, isHomeStackResizable);
+    void ensureNormalSplit() {
+        if (!inSplitMode()) {
+            // Wasn't in split-mode, so enter now.
+            if (DEBUG) {
+                Log.d(TAG, " enter split mode unminimized ");
+            }
+            mMinimized = false;
+            updateVisibility(true /* visible */);
         }
-
-        @Override
-        public void onAdjustedForImeChanged(boolean adjustedForIme, long animDuration)
-                throws RemoteException {
-            mView.post(() -> {
-                if (mAdjustedForIme != adjustedForIme) {
-                    mAdjustedForIme = adjustedForIme;
-                    updateTouchable();
-                    if (!mMinimized) {
-                        if (animDuration > 0) {
-                            mView.setAdjustedForIme(adjustedForIme, animDuration);
-                        } else {
-                            mView.setAdjustedForIme(adjustedForIme);
-                        }
-                    }
-                }
-            });
-        }
-
-        @Override
-        public void onDockSideChanged(final int newDockSide) throws RemoteException {
-            mView.post(() -> mView.notifyDockSideChanged(newDockSide));
+        if (mMinimized) {
+            // Was in minimized state, so leave that.
+            if (DEBUG) {
+                Log.d(TAG, " in split mode already, but unminimizing ");
+            }
+            mMinimized = false;
+            updateMinimizedDockedStack(mMinimized, getAnimDuration(),
+                    mHomeStackResizable);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerModule.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerModule.java
index 49f4d5e..3b7f315 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerModule.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerModule.java
@@ -17,8 +17,15 @@
 package com.android.systemui.stackdivider;
 
 import android.content.Context;
+import android.os.Handler;
 
+import com.android.systemui.TransactionPool;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.wm.DisplayController;
+import com.android.systemui.wm.DisplayImeController;
+import com.android.systemui.wm.SystemWindows;
 
 import java.util.Optional;
 
@@ -35,7 +42,11 @@
 public class DividerModule {
     @Singleton
     @Provides
-    static Divider provideDivider(Context context, Optional<Lazy<Recents>> recentsOptionalLazy) {
-        return new Divider(context, recentsOptionalLazy);
+    static Divider provideDivider(Context context, Optional<Lazy<Recents>> recentsOptionalLazy,
+            DisplayController displayController, SystemWindows systemWindows,
+            DisplayImeController imeController, @Main Handler handler,
+            KeyguardStateController keyguardStateController, TransactionPool transactionPool) {
+        return new Divider(context, recentsOptionalLazy, displayController, systemWindows,
+                imeController, handler, keyguardStateController, transactionPool);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
index 9fe6e84..375d9bb 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
@@ -16,12 +16,8 @@
 
 package com.android.systemui.stackdivider;
 
-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 android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
 import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
-import static android.view.WindowManager.DOCKED_LEFT;
 import static android.view.WindowManager.DOCKED_RIGHT;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
 
@@ -40,10 +36,11 @@
 import android.util.AttributeSet;
 import android.view.Choreographer;
 import android.view.Display;
-import android.view.DisplayInfo;
 import android.view.InsetsState;
 import android.view.MotionEvent;
 import android.view.PointerIcon;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.View.OnTouchListener;
@@ -75,6 +72,7 @@
  */
 public class DividerView extends FrameLayout implements OnTouchListener,
         OnComputeInternalInsetsListener {
+    private static final String TAG = "DividerView";
 
     public interface DividerCallbacks {
         void onDraggingStart();
@@ -123,14 +121,11 @@
     private int mTouchSlop;
     private boolean mBackgroundLifted;
     private boolean mIsInMinimizeInteraction;
-    private SnapTarget mSnapTargetBeforeMinimized;
+    SnapTarget mSnapTargetBeforeMinimized;
 
     private int mDividerInsets;
     private final Display mDefaultDisplay;
-    private int mDisplayWidth;
-    private int mDisplayHeight;
-    private int mDisplayRotation;
-    private int mDividerWindowWidth;
+
     private int mDividerSize;
     private int mTouchElevation;
     private int mLongPressEntraceAnimDuration;
@@ -147,8 +142,7 @@
     private DividerWindowManager mWindowManager;
     private VelocityTracker mVelocityTracker;
     private FlingAnimationUtils mFlingAnimationUtils;
-    private DividerSnapAlgorithm mSnapAlgorithm;
-    private DividerSnapAlgorithm mMinimizedSnapAlgorithm;
+    private SplitDisplayLayout mSplitLayout;
     private DividerCallbacks mCallback;
     private final Rect mStableInsets = new Rect();
 
@@ -163,6 +157,10 @@
     private DividerState mState;
     private final SurfaceFlingerVsyncChoreographer mSfChoreographer;
 
+    private SplitScreenTaskOrganizer mTiles;
+    boolean mFirstLayout = true;
+    int mDividerPositionX;
+    int mDividerPositionY;
 
     // The view is removed or in the process of been removed from the system.
     private boolean mRemoved;
@@ -172,7 +170,7 @@
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case MSG_RESIZE_STACK:
-                    resizeStack(msg.arg1, msg.arg2, (SnapTarget) msg.obj);
+                    resizeStackSurfaces(msg.arg1, msg.arg2, (SnapTarget) msg.obj);
                     break;
                 default:
                     super.handleMessage(msg);
@@ -228,16 +226,17 @@
         public boolean performAccessibilityAction(View host, int action, Bundle args) {
             int currentPosition = getCurrentPosition();
             SnapTarget nextTarget = null;
+            DividerSnapAlgorithm snapAlgorithm = mSplitLayout.getSnapAlgorithm();
             if (action == R.id.action_move_tl_full) {
-                nextTarget = mSnapAlgorithm.getDismissEndTarget();
+                nextTarget = snapAlgorithm.getDismissEndTarget();
             } else if (action == R.id.action_move_tl_70) {
-                nextTarget = mSnapAlgorithm.getLastSplitTarget();
+                nextTarget = snapAlgorithm.getLastSplitTarget();
             } else if (action == R.id.action_move_tl_50) {
-                nextTarget = mSnapAlgorithm.getMiddleTarget();
+                nextTarget = snapAlgorithm.getMiddleTarget();
             } else if (action == R.id.action_move_tl_30) {
-                nextTarget = mSnapAlgorithm.getFirstSplitTarget();
+                nextTarget = snapAlgorithm.getFirstSplitTarget();
             } else if (action == R.id.action_move_rb_full) {
-                nextTarget = mSnapAlgorithm.getDismissStartTarget();
+                nextTarget = snapAlgorithm.getDismissStartTarget();
             }
             if (nextTarget != null) {
                 startDragging(true /* animate */, false /* touching */);
@@ -284,11 +283,11 @@
         mBackground = findViewById(R.id.docked_divider_background);
         mMinimizedShadow = findViewById(R.id.minimized_dock_shadow);
         mHandle.setOnTouchListener(this);
-        mDividerWindowWidth = getResources().getDimensionPixelSize(
+        final int dividerWindowWidth = getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.docked_stack_divider_thickness);
         mDividerInsets = getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.docked_stack_divider_insets);
-        mDividerSize = mDividerWindowWidth - 2 * mDividerInsets;
+        mDividerSize = dividerWindowWidth - 2 * mDividerInsets;
         mTouchElevation = getResources().getDimensionPixelSize(
                 R.dimen.docked_stack_divider_lift_elevation);
         mLongPressEntraceAnimDuration = getResources().getInteger(
@@ -296,7 +295,6 @@
         mGrowRecents = getResources().getBoolean(R.bool.recents_grow_in_multiwindow);
         mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
         mFlingAnimationUtils = new FlingAnimationUtils(getResources().getDisplayMetrics(), 0.3f);
-        updateDisplayInfo();
         boolean landscape = getResources().getConfiguration().orientation
                 == Configuration.ORIENTATION_LANDSCAPE;
         mHandle.setPointerIcon(PointerIcon.getSystemIcon(getContext(),
@@ -314,6 +312,7 @@
                 && !mIsInMinimizeInteraction) {
             saveSnapTargetBeforeMinimized(mSnapTargetBeforeMinimized);
         }
+        mFirstLayout = true;
     }
 
     void onDividerRemoved() {
@@ -341,17 +340,17 @@
                 || mStableInsets.bottom != insets.getStableInsetBottom()) {
             mStableInsets.set(insets.getStableInsetLeft(), insets.getStableInsetTop(),
                     insets.getStableInsetRight(), insets.getStableInsetBottom());
-            if (mSnapAlgorithm != null || mMinimizedSnapAlgorithm != null) {
-                mSnapAlgorithm = null;
-                mMinimizedSnapAlgorithm = null;
-                initializeSnapAlgorithm();
-            }
         }
         return super.onApplyWindowInsets(insets);
     }
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (mFirstLayout) {
+            // Wait for first layout so that the ViewRootImpl surface has been created.
+            initializeSurfaceState();
+            mFirstLayout = false;
+        }
         super.onLayout(changed, left, top, right, bottom);
         int minimizeLeft = 0;
         int minimizeTop = 0;
@@ -372,19 +371,16 @@
     }
 
     public void injectDependencies(DividerWindowManager windowManager, DividerState dividerState,
-            DividerCallbacks callback) {
+            DividerCallbacks callback, SplitScreenTaskOrganizer tiles, SplitDisplayLayout sdl) {
         mWindowManager = windowManager;
         mState = dividerState;
         mCallback = callback;
-
-        // Set the previous position ratio before minimized state after attaching this divider
-        if (mStableInsets.isEmpty()) {
-            WindowManagerWrapper.getInstance().getStableInsets(mStableInsets);
-        }
+        mTiles = tiles;
+        mSplitLayout = sdl;
 
         if (mState.mRatioPositionBeforeMinimized == 0) {
             // Set the middle target as the initial state
-            mSnapTargetBeforeMinimized = mSnapAlgorithm.getMiddleTarget();
+            mSnapTargetBeforeMinimized = mSplitLayout.getSnapAlgorithm().getMiddleTarget();
         } else {
             repositionSnapTargetBeforeMinimized();
         }
@@ -411,18 +407,35 @@
         return mOtherTaskRect;
     }
 
+    private boolean inSplitMode() {
+        return getVisibility() == VISIBLE;
+    }
+
+    /** Unlike setVisible, this directly hides the surface without changing view visibility. */
+    void setHidden(boolean hidden) {
+        post(() -> {
+            final SurfaceControl sc = getWindowSurfaceControl();
+            if (sc == null) {
+                return;
+            }
+            Transaction t = mTiles.getTransaction();
+            if (hidden) {
+                t.hide(sc);
+            } else {
+                t.show(sc);
+            }
+            t.apply();
+            mTiles.releaseTransaction(t);
+        });
+    }
+
     public boolean startDragging(boolean animate, boolean touching) {
         cancelFlingAnimation();
         if (touching) {
             mHandle.setTouching(true, animate);
         }
-        mDockSide = mWindowManagerProxy.getDockSide();
+        mDockSide = mSplitLayout.getPrimarySplitSide();
 
-        // Update snap algorithm if rotation has occurred
-        if (mDisplayRotation != mDefaultDisplay.getRotation()) {
-            updateDisplayInfo();
-        }
-        initializeSnapAlgorithm();
         mWindowManagerProxy.setResizing(true);
         if (touching) {
             mWindowManager.setSlippery(false);
@@ -431,7 +444,7 @@
         if (mCallback != null) {
             mCallback.onDraggingStart();
         }
-        return mDockSide != WindowManager.DOCKED_INVALID;
+        return inSplitMode();
     }
 
     public void stopDragging(int position, float velocity, boolean avoidDismissStart,
@@ -467,38 +480,22 @@
     }
 
     private void updateDockSide() {
-        mDockSide = mWindowManagerProxy.getDockSide();
+        mDockSide = mSplitLayout.getPrimarySplitSide();
         mMinimizedShadow.setDockSide(mDockSide);
     }
 
-    private void initializeSnapAlgorithm() {
-        if (mSnapAlgorithm == null) {
-            mSnapAlgorithm = new DividerSnapAlgorithm(getContext().getResources(), mDisplayWidth,
-                    mDisplayHeight, mDividerSize, isHorizontalDivision(), mStableInsets, mDockSide);
-            if (mSnapTargetBeforeMinimized != null && mSnapTargetBeforeMinimized.isMiddleTarget) {
-                mSnapTargetBeforeMinimized = mSnapAlgorithm.getMiddleTarget();
-            }
-        }
-        if (mMinimizedSnapAlgorithm == null) {
-            mMinimizedSnapAlgorithm = new DividerSnapAlgorithm(getContext().getResources(),
-                    mDisplayWidth, mDisplayHeight, mDividerSize, isHorizontalDivision(),
-                    mStableInsets, mDockSide, mDockedStackMinimized && mHomeStackResizable);
-        }
-    }
-
     public DividerSnapAlgorithm getSnapAlgorithm() {
-        initializeSnapAlgorithm();
-        return mDockedStackMinimized && mHomeStackResizable ? mMinimizedSnapAlgorithm :
-                mSnapAlgorithm;
+        return mDockedStackMinimized
+                && mHomeStackResizable ? mSplitLayout.getMinimizedSnapAlgorithm()
+                        : mSplitLayout.getSnapAlgorithm();
     }
 
     public int getCurrentPosition() {
-        getLocationOnScreen(mTempInt2);
-        if (isHorizontalDivision()) {
-            return mTempInt2[1] + mDividerInsets;
-        } else {
-            return mTempInt2[0] + mDividerInsets;
-        }
+        return isHorizontalDivision() ? mDividerPositionY : mDividerPositionX;
+    }
+
+    public boolean isMinimized() {
+        return mDockedStackMinimized;
     }
 
     @Override
@@ -557,25 +554,25 @@
     }
 
     private void logResizeEvent(SnapTarget snapTarget) {
-        if (snapTarget == mSnapAlgorithm.getDismissStartTarget()) {
+        if (snapTarget == mSplitLayout.getSnapAlgorithm().getDismissStartTarget()) {
             MetricsLogger.action(
                     mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_MAX, dockSideTopLeft(mDockSide)
                             ? LOG_VALUE_UNDOCK_MAX_OTHER
                             : LOG_VALUE_UNDOCK_MAX_DOCKED);
-        } else if (snapTarget == mSnapAlgorithm.getDismissEndTarget()) {
+        } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getDismissEndTarget()) {
             MetricsLogger.action(
                     mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_MAX, dockSideBottomRight(mDockSide)
                             ? LOG_VALUE_UNDOCK_MAX_OTHER
                             : LOG_VALUE_UNDOCK_MAX_DOCKED);
-        } else if (snapTarget == mSnapAlgorithm.getMiddleTarget()) {
+        } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getMiddleTarget()) {
             MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE,
                     LOG_VALUE_RESIZE_50_50);
-        } else if (snapTarget == mSnapAlgorithm.getFirstSplitTarget()) {
+        } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getFirstSplitTarget()) {
             MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE,
                     dockSideTopLeft(mDockSide)
                             ? LOG_VALUE_RESIZE_DOCKED_SMALLER
                             : LOG_VALUE_RESIZE_DOCKED_LARGER);
-        } else if (snapTarget == mSnapAlgorithm.getLastSplitTarget()) {
+        } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getLastSplitTarget()) {
             MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE,
                     dockSideTopLeft(mDockSide)
                             ? LOG_VALUE_RESIZE_DOCKED_LARGER
@@ -625,12 +622,16 @@
                         : snapTarget.taskPosition,
                 snapTarget));
         Runnable endAction = () -> {
-            commitSnapFlags(snapTarget);
+            boolean dismissed = commitSnapFlags(snapTarget);
             mWindowManagerProxy.setResizing(false);
             updateDockSide();
             mCurrentAnimator = null;
             mEntranceAnimationRunning = false;
             mExitAnimationRunning = false;
+            if (!dismissed) {
+                WindowManagerProxy.applyResizeSplits((mIsInMinimizeInteraction
+                        ? mSnapTargetBeforeMinimized : snapTarget).position, mSplitLayout);
+            }
             if (mCallback != null) {
                 mCallback.onDraggingEnd();
             }
@@ -642,12 +643,13 @@
                 // position isn't negative.
                 final SnapTarget saveTarget;
                 if (snapTarget.position < 0) {
-                    saveTarget = mSnapAlgorithm.getMiddleTarget();
+                    saveTarget = mSplitLayout.getSnapAlgorithm().getMiddleTarget();
                 } else {
                     saveTarget = snapTarget;
                 }
-                if (saveTarget.position != mSnapAlgorithm.getDismissEndTarget().position
-                        && saveTarget.position != mSnapAlgorithm.getDismissStartTarget().position) {
+                final DividerSnapAlgorithm snapAlgo = mSplitLayout.getSnapAlgorithm();
+                if (saveTarget.position != snapAlgo.getDismissEndTarget().position
+                        && saveTarget.position != snapAlgo.getDismissStartTarget().position) {
                     saveSnapTargetBeforeMinimized(saveTarget);
                 }
             }
@@ -701,11 +703,11 @@
         }
     }
 
-    private void commitSnapFlags(SnapTarget target) {
+    private boolean commitSnapFlags(SnapTarget target) {
         if (target.flag == SnapTarget.FLAG_NONE) {
-            return;
+            return false;
         }
-        boolean dismissOrMaximize;
+        final boolean dismissOrMaximize;
         if (target.flag == SnapTarget.FLAG_DISMISS_START) {
             dismissOrMaximize = mDockSide == WindowManager.DOCKED_LEFT
                     || mDockSide == WindowManager.DOCKED_TOP;
@@ -713,12 +715,13 @@
             dismissOrMaximize = mDockSide == WindowManager.DOCKED_RIGHT
                     || mDockSide == WindowManager.DOCKED_BOTTOM;
         }
-        if (dismissOrMaximize) {
-            mWindowManagerProxy.dismissDockedStack();
-        } else {
-            mWindowManagerProxy.maximizeDockedStack();
-        }
-        mWindowManagerProxy.setResizeDimLayer(false, WINDOWING_MODE_UNDEFINED, 0f);
+        mWindowManagerProxy.dismissOrMaximizeDocked(mTiles, dismissOrMaximize);
+        Transaction t = mTiles.getTransaction();
+        setResizeDimLayer(t, true /* primary */, 0f);
+        setResizeDimLayer(t, false /* primary */, 0f);
+        t.apply();
+        mTiles.releaseTransaction(t);
+        return true;
     }
 
     private void liftBackground() {
@@ -765,6 +768,28 @@
         mBackgroundLifted = false;
     }
 
+    private void initializeSurfaceState() {
+        int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
+        // Recalculate the split-layout's internal tile bounds
+        mSplitLayout.resizeSplits(midPos);
+        Transaction t = mTiles.getTransaction();
+        if (mDockedStackMinimized) {
+            int position = mSplitLayout.getMinimizedSnapAlgorithm().getMiddleTarget().position;
+            calculateBoundsForPosition(position, mDockSide, mDockedRect);
+            calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide),
+                    mOtherRect);
+            mDividerPositionX = mDividerPositionY = position;
+            resizeSplitSurfaces(t, mDockedRect, mSplitLayout.mPrimary,
+                    mOtherRect, mSplitLayout.mSecondary);
+        } else {
+            resizeSplitSurfaces(t, mSplitLayout.mPrimary, null,
+                    mSplitLayout.mSecondary, null);
+        }
+        setResizeDimLayer(t, true /* primary */, 0.f /* alpha */);
+        setResizeDimLayer(t, false /* secondary */, 0.f /* alpha */);
+        t.apply();
+        mTiles.releaseTransaction(t);
+    }
 
     public void setMinimizedDockStack(boolean minimized, boolean isHomeStackResizable) {
         mHomeStackResizable = isHomeStackResizable;
@@ -789,15 +814,11 @@
             mDockedStackMinimized = minimized;
         } else if (mDockedStackMinimized != minimized) {
             mDockedStackMinimized = minimized;
-            if (mDisplayRotation != mDefaultDisplay.getRotation()) {
+            if (mSplitLayout.mDisplayLayout.rotation() != mDefaultDisplay.getRotation()) {
                 // Splitscreen to minimize is about to starts after rotating landscape to seascape,
                 // update insets, display info and snap algorithm targets
                 WindowManagerWrapper.getInstance().getStableInsets(mStableInsets);
                 repositionSnapTargetBeforeMinimized();
-                updateDisplayInfo();
-            } else {
-                mMinimizedSnapAlgorithm = null;
-                initializeSnapAlgorithm();
             }
             if (mIsInMinimizeInteraction != minimized || mCurrentAnimator != null) {
                 cancelFlingAnimation();
@@ -805,15 +826,64 @@
                     // Relayout to recalculate the divider shadow when minimizing
                     requestLayout();
                     mIsInMinimizeInteraction = true;
-                    resizeStack(mMinimizedSnapAlgorithm.getMiddleTarget());
+                    resizeStackSurfaces(mSplitLayout.getMinimizedSnapAlgorithm().getMiddleTarget());
                 } else {
-                    resizeStack(mSnapTargetBeforeMinimized);
+                    resizeStackSurfaces(mSnapTargetBeforeMinimized);
                     mIsInMinimizeInteraction = false;
                 }
             }
         }
     }
 
+    void enterSplitMode(boolean isHomeStackResizable) {
+        post(() -> {
+            final SurfaceControl sc = getWindowSurfaceControl();
+            if (sc == null) {
+                return;
+            }
+            Transaction t = mTiles.getTransaction();
+            t.show(sc).apply();
+            mTiles.releaseTransaction(t);
+        });
+        if (isHomeStackResizable) {
+            SnapTarget miniMid = mSplitLayout.getMinimizedSnapAlgorithm().getMiddleTarget();
+            if (mDockedStackMinimized) {
+                mDividerPositionY = mDividerPositionX = miniMid.position;
+            }
+        }
+    }
+
+    /**
+     * Tries to grab a surface control from ViewRootImpl. If this isn't available for some reason
+     * (ie. the window isn't ready yet), it will get the surfacecontrol that the WindowlessWM has
+     * assigned to it.
+     */
+    private SurfaceControl getWindowSurfaceControl() {
+        if (getViewRootImpl() == null) {
+            return null;
+        }
+        SurfaceControl out = getViewRootImpl().getSurfaceControl();
+        if (out != null && out.isValid()) {
+            return out;
+        }
+        return mWindowManager.mSystemWindows.getViewSurface(this);
+    }
+
+    void exitSplitMode() {
+        // Reset tile bounds
+        post(() -> {
+            final SurfaceControl sc = getWindowSurfaceControl();
+            if (sc == null) {
+                return;
+            }
+            Transaction t = mTiles.getTransaction();
+            t.hide(sc).apply();
+            mTiles.releaseTransaction(t);
+        });
+        int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
+        WindowManagerProxy.applyResizeSplits(midPos, mSplitLayout);
+    }
+
     public void setMinimizedDockStack(boolean minimized, long animDuration,
             boolean isHomeStackResizable) {
         mHomeStackResizable = isHomeStackResizable;
@@ -844,14 +914,12 @@
             mDockedStackMinimized = minimized;
         } else if (mDockedStackMinimized != minimized) {
             mIsInMinimizeInteraction = true;
-            mMinimizedSnapAlgorithm = null;
             mDockedStackMinimized = minimized;
-            initializeSnapAlgorithm();
             stopDragging(minimized
                             ? mSnapTargetBeforeMinimized.position
                             : getCurrentPosition(),
                     minimized
-                            ? mMinimizedSnapAlgorithm.getMiddleTarget()
+                            ? mSplitLayout.getMinimizedSnapAlgorithm().getMiddleTarget()
                             : mSnapTargetBeforeMinimized,
                     animDuration, Interpolators.FAST_OUT_SLOW_IN, 0);
             setAdjustedForIme(false, animDuration);
@@ -865,18 +933,6 @@
                 .start();
     }
 
-    public void setAdjustedForIme(boolean adjustedForIme) {
-        updateDockSide();
-        mHandle.setAlpha(adjustedForIme ? 0f : 1f);
-        if (!adjustedForIme) {
-            resetBackground();
-        } else if (mDockSide == WindowManager.DOCKED_TOP) {
-            mBackground.setPivotY(0);
-            mBackground.setScaleY(ADJUSTED_FOR_IME_SCALE);
-        }
-        mAdjustedForIme = adjustedForIme;
-    }
-
     public void setAdjustedForIme(boolean adjustedForIme, long animDuration) {
         updateDockSide();
         mHandle.animate()
@@ -902,7 +958,8 @@
     private void saveSnapTargetBeforeMinimized(SnapTarget target) {
         mSnapTargetBeforeMinimized = target;
         mState.mRatioPositionBeforeMinimized = (float) target.position /
-                (isHorizontalDivision() ? mDisplayHeight : mDisplayWidth);
+                (isHorizontalDivision() ? mSplitLayout.mDisplayLayout.height()
+                        : mSplitLayout.mDisplayLayout.width());
     }
 
     private void resetBackground() {
@@ -916,51 +973,17 @@
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        updateDisplayInfo();
-    }
-
-    public void notifyDockSideChanged(int newDockSide) {
-        int oldDockSide = mDockSide;
-        mDockSide = newDockSide;
-        mMinimizedShadow.setDockSide(mDockSide);
-        requestLayout();
-
-        // Update the snap position to the new docked side with correct insets
-        WindowManagerWrapper.getInstance().getStableInsets(mStableInsets);
-        mMinimizedSnapAlgorithm = null;
-        initializeSnapAlgorithm();
-
-        if (oldDockSide == DOCKED_LEFT && mDockSide == DOCKED_RIGHT
-                || oldDockSide == DOCKED_RIGHT && mDockSide == DOCKED_LEFT) {
-            repositionSnapTargetBeforeMinimized();
-        }
-
-        // Landscape to seascape rotation requires minimized to resize docked app correctly
-        if (mHomeStackResizable && mDockedStackMinimized) {
-            resizeStack(mMinimizedSnapAlgorithm.getMiddleTarget());
-        }
     }
 
     private void repositionSnapTargetBeforeMinimized() {
         int position = (int) (mState.mRatioPositionBeforeMinimized *
-                (isHorizontalDivision() ? mDisplayHeight : mDisplayWidth));
-        mSnapAlgorithm = null;
-        initializeSnapAlgorithm();
+                (isHorizontalDivision() ? mSplitLayout.mDisplayLayout.height()
+                        : mSplitLayout.mDisplayLayout.width()));
 
         // Set the snap target before minimized but do not save until divider is attached and not
         // minimized because it does not know its minimized state yet.
-        mSnapTargetBeforeMinimized = mSnapAlgorithm.calculateNonDismissingSnapTarget(position);
-    }
-
-    private void updateDisplayInfo() {
-        mDisplayRotation = mDefaultDisplay.getRotation();
-        final DisplayInfo info = new DisplayInfo();
-        mDefaultDisplay.getDisplayInfo(info);
-        mDisplayWidth = info.logicalWidth;
-        mDisplayHeight = info.logicalHeight;
-        mSnapAlgorithm = null;
-        mMinimizedSnapAlgorithm = null;
-        initializeSnapAlgorithm();
+        mSnapTargetBeforeMinimized =
+                mSplitLayout.getSnapAlgorithm().calculateNonDismissingSnapTarget(position);
     }
 
     private int calculatePosition(int touchX, int touchY) {
@@ -994,8 +1017,9 @@
     }
 
     public void calculateBoundsForPosition(int position, int dockSide, Rect outRect) {
-        DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outRect, mDisplayWidth,
-                mDisplayHeight, mDividerSize);
+        DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outRect,
+                mSplitLayout.mDisplayLayout.width(), mSplitLayout.mDisplayLayout.height(),
+                mDividerSize);
     }
 
     public void resizeStackDelayed(int position, int taskPosition, SnapTarget taskSnapTarget) {
@@ -1005,16 +1029,60 @@
         mSfChoreographer.scheduleAtSfVsync(mHandler, message);
     }
 
-    private void resizeStack(SnapTarget taskSnapTarget) {
-        resizeStack(taskSnapTarget.position, taskSnapTarget.position, taskSnapTarget);
+    private void resizeStackSurfaces(SnapTarget taskSnapTarget) {
+        resizeStackSurfaces(taskSnapTarget.position, taskSnapTarget.position, taskSnapTarget);
     }
 
-    public void resizeStack(int position, int taskPosition, SnapTarget taskSnapTarget) {
+    void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect otherRect) {
+        resizeSplitSurfaces(t, dockedRect, null, otherRect, null);
+    }
+
+    private void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect dockedTaskRect,
+            Rect otherRect, Rect otherTaskRect) {
+        dockedTaskRect = dockedTaskRect == null ? dockedRect : dockedTaskRect;
+        otherTaskRect = otherTaskRect == null ? otherRect : otherTaskRect;
+
+        mDividerPositionX = dockedRect.right;
+        mDividerPositionY = dockedRect.bottom;
+
+        t.setPosition(mTiles.mPrimarySurface, dockedTaskRect.left, dockedTaskRect.top);
+        Rect crop = new Rect(dockedRect);
+        crop.offsetTo(-Math.min(dockedTaskRect.left - dockedRect.left, 0),
+                -Math.min(dockedTaskRect.top - dockedRect.top, 0));
+        t.setWindowCrop(mTiles.mPrimarySurface, crop);
+        t.setPosition(mTiles.mSecondarySurface, otherTaskRect.left, otherTaskRect.top);
+        crop.set(otherRect);
+        crop.offsetTo(-(otherTaskRect.left - otherRect.left),
+                -(otherTaskRect.top - otherRect.top));
+        t.setWindowCrop(mTiles.mSecondarySurface, crop);
+        final SurfaceControl dividerCtrl = getWindowSurfaceControl();
+        if (dividerCtrl != null) {
+            if (isHorizontalDivision()) {
+                t.setPosition(dividerCtrl, 0, mDividerPositionY - mDividerInsets);
+            } else {
+                t.setPosition(dividerCtrl, mDividerPositionX - mDividerInsets, 0);
+            }
+        }
+    }
+
+    void setResizeDimLayer(Transaction t, boolean primary, float alpha) {
+        SurfaceControl dim = primary ? mTiles.mPrimaryDim : mTiles.mSecondaryDim;
+        if (alpha <= 0.f) {
+            t.hide(dim);
+        } else {
+            t.setAlpha(dim, alpha);
+            t.show(dim);
+        }
+    }
+
+    void resizeStackSurfaces(int position, int taskPosition, SnapTarget taskSnapTarget) {
         if (mRemoved) {
             // This divider view has been removed so shouldn't have any additional influence.
             return;
         }
         calculateBoundsForPosition(position, mDockSide, mDockedRect);
+        calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide),
+                mOtherRect);
 
         if (mDockedRect.equals(mLastResizeRect) && !mEntranceAnimationRunning) {
             return;
@@ -1025,6 +1093,7 @@
             mBackground.invalidate();
         }
 
+        Transaction t = mTiles.getTransaction();
         mLastResizeRect.set(mDockedRect);
         if (mHomeStackResizable && mIsInMinimizeInteraction) {
             calculateBoundsForPosition(mSnapTargetBeforeMinimized.position, mDockSide,
@@ -1037,8 +1106,10 @@
                 mDockedTaskRect.offset(Math.max(position, mStableInsets.left - mDividerSize)
                         - mDockedTaskRect.left + mDividerSize, 0);
             }
-            mWindowManagerProxy.resizeDockedStack(mDockedRect, mDockedTaskRect, mDockedTaskRect,
-                    mOtherTaskRect, null);
+            resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect,
+                    mOtherTaskRect);
+            t.apply();
+            mTiles.releaseTransaction(t);
             return;
         }
 
@@ -1052,8 +1123,7 @@
             }
             calculateBoundsForPosition(taskPosition, DockedDividerUtils.invertDockSide(mDockSide),
                     mOtherTaskRect);
-            mWindowManagerProxy.resizeDockedStack(mDockedRect, mDockedTaskRect, null,
-                    mOtherTaskRect, null);
+            resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
         } else if (mExitAnimationRunning && taskPosition != TASK_POSITION_SAME) {
             calculateBoundsForPosition(taskPosition, mDockSide, mDockedTaskRect);
             mDockedInsetRect.set(mDockedTaskRect);
@@ -1066,8 +1136,7 @@
             if (mDockSide == DOCKED_RIGHT) {
                 mDockedTaskRect.offset(position - mStableInsets.left + mDividerSize, 0);
             }
-            mWindowManagerProxy.resizeDockedStack(mDockedRect, mDockedTaskRect, mDockedInsetRect,
-                    mOtherTaskRect, mOtherInsetRect);
+            resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
         } else if (taskPosition != TASK_POSITION_SAME) {
             calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide),
                     mOtherRect);
@@ -1078,7 +1147,8 @@
                     restrictDismissingTaskPosition(taskPosition, dockSideInverted, taskSnapTarget);
             calculateBoundsForPosition(taskPositionDocked, mDockSide, mDockedTaskRect);
             calculateBoundsForPosition(taskPositionOther, dockSideInverted, mOtherTaskRect);
-            mTmpRect.set(0, 0, mDisplayWidth, mDisplayHeight);
+            mTmpRect.set(0, 0, mSplitLayout.mDisplayLayout.width(),
+                    mSplitLayout.mDisplayLayout.height());
             alignTopLeft(mDockedRect, mDockedTaskRect);
             alignTopLeft(mOtherRect, mOtherTaskRect);
             mDockedInsetRect.set(mDockedTaskRect);
@@ -1094,15 +1164,15 @@
                     taskPositionDocked);
             applyDismissingParallax(mOtherTaskRect, dockSideInverted, taskSnapTarget, position,
                     taskPositionOther);
-            mWindowManagerProxy.resizeDockedStack(mDockedRect, mDockedTaskRect, mDockedInsetRect,
-                    mOtherTaskRect, mOtherInsetRect);
+            resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
         } else {
-            mWindowManagerProxy.resizeDockedStack(mDockedRect, null, null, null, null);
+            resizeSplitSurfaces(t, mDockedRect, null, mOtherRect, null);
         }
         SnapTarget closestDismissTarget = getSnapAlgorithm().getClosestDismissTarget(position);
         float dimFraction = getDimFraction(position, closestDismissTarget);
-        mWindowManagerProxy.setResizeDimLayer(dimFraction != 0f,
-                getWindowingModeForDismissTarget(closestDismissTarget), dimFraction);
+        setResizeDimLayer(t, isDismissTargetPrimary(closestDismissTarget), dimFraction);
+        t.apply();
+        mTiles.releaseTransaction(t);
     }
 
     private void applyExitAnimationParallax(Rect taskRect, int position) {
@@ -1156,10 +1226,12 @@
     private int restrictDismissingTaskPosition(int taskPosition, int dockSide,
             SnapTarget snapTarget) {
         if (snapTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(dockSide)) {
-            return Math.max(mSnapAlgorithm.getFirstSplitTarget().position, mStartPosition);
+            return Math.max(mSplitLayout.getSnapAlgorithm().getFirstSplitTarget().position,
+                    mStartPosition);
         } else if (snapTarget.flag == SnapTarget.FLAG_DISMISS_END
                 && dockSideBottomRight(dockSide)) {
-            return Math.min(mSnapAlgorithm.getLastSplitTarget().position, mStartPosition);
+            return Math.min(mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position,
+                    mStartPosition);
         } else {
             return taskPosition;
         }
@@ -1171,19 +1243,19 @@
     private void applyDismissingParallax(Rect taskRect, int dockSide, SnapTarget snapTarget,
             int position, int taskPosition) {
         float fraction = Math.min(1, Math.max(0,
-                mSnapAlgorithm.calculateDismissingFraction(position)));
+                mSplitLayout.getSnapAlgorithm().calculateDismissingFraction(position)));
         SnapTarget dismissTarget = null;
         SnapTarget splitTarget = null;
         int start = 0;
-        if (position <= mSnapAlgorithm.getLastSplitTarget().position
+        if (position <= mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position
                 && dockSideTopLeft(dockSide)) {
-            dismissTarget = mSnapAlgorithm.getDismissStartTarget();
-            splitTarget = mSnapAlgorithm.getFirstSplitTarget();
+            dismissTarget = mSplitLayout.getSnapAlgorithm().getDismissStartTarget();
+            splitTarget = mSplitLayout.getSnapAlgorithm().getFirstSplitTarget();
             start = taskPosition;
-        } else if (position >= mSnapAlgorithm.getLastSplitTarget().position
+        } else if (position >= mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position
                 && dockSideBottomRight(dockSide)) {
-            dismissTarget = mSnapAlgorithm.getDismissEndTarget();
-            splitTarget = mSnapAlgorithm.getLastSplitTarget();
+            dismissTarget = mSplitLayout.getSnapAlgorithm().getDismissEndTarget();
+            splitTarget = mSplitLayout.getSnapAlgorithm().getLastSplitTarget();
             start = splitTarget.position;
         }
         if (dismissTarget != null && fraction > 0f
@@ -1236,14 +1308,10 @@
         }
     }
 
-    private int getWindowingModeForDismissTarget(SnapTarget dismissTarget) {
-        if ((dismissTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(mDockSide))
+    private boolean isDismissTargetPrimary(SnapTarget dismissTarget) {
+        return (dismissTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(mDockSide))
                 || (dismissTarget.flag == SnapTarget.FLAG_DISMISS_END
-                        && dockSideBottomRight(mDockSide))) {
-            return WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-        } else {
-            return WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
-        }
+                        && dockSideBottomRight(mDockSide));
     }
 
     /**
@@ -1297,7 +1365,7 @@
     }
 
     void onDockedFirstAnimationFrame() {
-        saveSnapTargetBeforeMinimized(mSnapAlgorithm.getMiddleTarget());
+        saveSnapTargetBeforeMinimized(mSplitLayout.getSnapAlgorithm().getMiddleTarget());
     }
 
     void onDockedTopTask() {
@@ -1307,8 +1375,9 @@
         updateDockSide();
         mEntranceAnimationRunning = true;
 
-        resizeStack(calculatePositionForInsetBounds(), mSnapAlgorithm.getMiddleTarget().position,
-                mSnapAlgorithm.getMiddleTarget());
+        resizeStackSurfaces(calculatePositionForInsetBounds(),
+                mSplitLayout.getSnapAlgorithm().getMiddleTarget().position,
+                mSplitLayout.getSnapAlgorithm().getMiddleTarget());
     }
 
     void onRecentsDrawn() {
@@ -1337,13 +1406,12 @@
     }
 
     void onUndockingTask() {
-        int dockSide = mWindowManagerProxy.getDockSide();
-        if (dockSide != WindowManager.DOCKED_INVALID && (mHomeStackResizable
-                || !mDockedStackMinimized)) {
+        int dockSide = mSplitLayout.getPrimarySplitSide();
+        if (inSplitMode() && (mHomeStackResizable || !mDockedStackMinimized)) {
             startDragging(false /* animate */, false /* touching */);
             SnapTarget target = dockSideTopLeft(dockSide)
-                    ? mSnapAlgorithm.getDismissEndTarget()
-                    : mSnapAlgorithm.getDismissStartTarget();
+                    ? mSplitLayout.getSnapAlgorithm().getDismissEndTarget()
+                    : mSplitLayout.getSnapAlgorithm().getDismissStartTarget();
 
             // Don't start immediately - give a little bit time to settle the drag resize change.
             mExitAnimationRunning = true;
@@ -1354,8 +1422,7 @@
     }
 
     private int calculatePositionForInsetBounds() {
-        mTmpRect.set(0, 0, mDisplayWidth, mDisplayHeight);
-        mTmpRect.inset(mStableInsets);
+        mSplitLayout.mDisplayLayout.getStableBounds(mTmpRect);
         return DockedDividerUtils.calculatePositionForBounds(mTmpRect, mDockSide, mDividerSize);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerWindowManager.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerWindowManager.java
index 2486d653..3020a25 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerWindowManager.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerWindowManager.java
@@ -26,12 +26,13 @@
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 
-import android.content.Context;
 import android.graphics.PixelFormat;
 import android.os.Binder;
 import android.view.View;
 import android.view.WindowManager;
 
+import com.android.systemui.wm.SystemWindows;
+
 /**
  * Manages the window parameters of the docked stack divider.
  */
@@ -39,15 +40,16 @@
 
     private static final String WINDOW_TITLE = "DockedStackDivider";
 
-    private final WindowManager mWindowManager;
+    final SystemWindows mSystemWindows;
     private WindowManager.LayoutParams mLp;
     private View mView;
 
-    public DividerWindowManager(Context ctx) {
-        mWindowManager = ctx.getSystemService(WindowManager.class);
+    public DividerWindowManager(SystemWindows systemWindows) {
+        mSystemWindows = systemWindows;
     }
 
-    public void add(View view, int width, int height) {
+    /** Add a divider view */
+    public void add(View view, int width, int height, int displayId) {
         mLp = new WindowManager.LayoutParams(
                 width, height, TYPE_DOCK_DIVIDER,
                 FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL
@@ -60,13 +62,13 @@
         view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
-        mWindowManager.addView(view, mLp);
+        mSystemWindows.addView(view, mLp, displayId, TYPE_DOCK_DIVIDER);
         mView = view;
     }
 
     public void remove() {
         if (mView != null) {
-            mWindowManager.removeView(mView);
+            mSystemWindows.removeView(mView);
         }
         mView = null;
     }
@@ -81,7 +83,7 @@
             changed = true;
         }
         if (changed) {
-            mWindowManager.updateViewLayout(mView, mLp);
+            mSystemWindows.updateViewLayout(mView, mLp);
         }
     }
 
@@ -95,7 +97,7 @@
             changed = true;
         }
         if (changed) {
-            mWindowManager.updateViewLayout(mView, mLp);
+            mSystemWindows.updateViewLayout(mView, mLp);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/ForcedResizableInfoActivityController.java b/packages/SystemUI/src/com/android/systemui/stackdivider/ForcedResizableInfoActivityController.java
index c6ac309..db7996e 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/ForcedResizableInfoActivityController.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/ForcedResizableInfoActivityController.java
@@ -31,6 +31,8 @@
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 
+import java.util.function.Consumer;
+
 /**
  * Controller that decides when to show the {@link ForcedResizableInfoActivity}.
  */
@@ -52,6 +54,12 @@
         }
     };
 
+    private final Consumer<Boolean> mDockedStackExistsListener = exists -> {
+        if (!exists) {
+            mPackagesShownInSession.clear();
+        }
+    };
+
     /** Record of force resized task that's pending to be handled. */
     private class PendingTaskRecord {
         int taskId;
@@ -67,7 +75,7 @@
         }
     }
 
-    public ForcedResizableInfoActivityController(Context context) {
+    public ForcedResizableInfoActivityController(Context context, Divider divider) {
         mContext = context;
         ActivityManagerWrapper.getInstance().registerTaskStackListener(
                 new TaskStackChangeListener() {
@@ -87,12 +95,7 @@
                         activityLaunchOnSecondaryDisplayFailed();
                     }
                 });
-    }
-
-    public void notifyDockedStackExistsChanged(boolean exists) {
-        if (!exists) {
-            mPackagesShownInSession.clear();
-        }
+        divider.registerInSplitScreenListener(mDockedStackExistsListener);
     }
 
     public void onAppTransitionFinished() {
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitDisplayLayout.java b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitDisplayLayout.java
new file mode 100644
index 0000000..b19f560
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitDisplayLayout.java
@@ -0,0 +1,310 @@
+/*
+ * 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.systemui.stackdivider;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.WindowManager.DOCKED_BOTTOM;
+import static android.view.WindowManager.DOCKED_INVALID;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_RIGHT;
+import static android.view.WindowManager.DOCKED_TOP;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.util.TypedValue;
+import android.view.WindowContainerTransaction;
+
+import com.android.internal.policy.DividerSnapAlgorithm;
+import com.android.internal.policy.DockedDividerUtils;
+import com.android.systemui.wm.DisplayLayout;
+
+/**
+ * Handles split-screen related internal display layout. In general, this represents the
+ * WM-facing understanding of the splits.
+ */
+public class SplitDisplayLayout {
+    /** Minimum size of an adjusted stack bounds relative to original stack bounds. Used to
+     * restrict IME adjustment so that a min portion of top stack remains visible.*/
+    private static final float ADJUSTED_STACK_FRACTION_MIN = 0.3f;
+
+    private static final int DIVIDER_WIDTH_INACTIVE_DP = 4;
+
+    SplitScreenTaskOrganizer mTiles;
+    DisplayLayout mDisplayLayout;
+    Context mContext;
+
+    // Lazy stuff
+    boolean mResourcesValid = false;
+    int mDividerSize;
+    int mDividerSizeInactive;
+    private DividerSnapAlgorithm mSnapAlgorithm = null;
+    private DividerSnapAlgorithm mMinimizedSnapAlgorithm = null;
+    Rect mPrimary = null;
+    Rect mSecondary = null;
+    Rect mAdjustedPrimary = null;
+    Rect mAdjustedSecondary = null;
+
+    public SplitDisplayLayout(Context ctx, DisplayLayout dl, SplitScreenTaskOrganizer taskTiles) {
+        mTiles = taskTiles;
+        mDisplayLayout = dl;
+        mContext = ctx;
+    }
+
+    void rotateTo(int newRotation) {
+        mDisplayLayout.rotateTo(mContext.getResources(), newRotation);
+        final Configuration config = new Configuration();
+        config.unset();
+        config.orientation = mDisplayLayout.getOrientation();
+        Rect tmpRect = new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
+        tmpRect.inset(mDisplayLayout.nonDecorInsets());
+        config.windowConfiguration.setAppBounds(tmpRect);
+        tmpRect.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
+        tmpRect.inset(mDisplayLayout.stableInsets());
+        config.screenWidthDp = (int) (tmpRect.width() / mDisplayLayout.density());
+        config.screenHeightDp = (int) (tmpRect.height() / mDisplayLayout.density());
+        mContext = mContext.createConfigurationContext(config);
+        mSnapAlgorithm = null;
+        mMinimizedSnapAlgorithm = null;
+        mResourcesValid = false;
+    }
+
+    private void updateResources() {
+        if (mResourcesValid) {
+            return;
+        }
+        mResourcesValid = true;
+        Resources res = mContext.getResources();
+        mDividerSize = DockedDividerUtils.getDividerSize(res,
+                DockedDividerUtils.getDividerInsets(res));
+        mDividerSizeInactive = (int) TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP, DIVIDER_WIDTH_INACTIVE_DP, res.getDisplayMetrics());
+    }
+
+    int getPrimarySplitSide() {
+        return mDisplayLayout.isLandscape() ? DOCKED_LEFT : DOCKED_TOP;
+    }
+
+    boolean isMinimized() {
+        return mTiles.mSecondary.topActivityType == ACTIVITY_TYPE_HOME
+                || mTiles.mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS;
+    }
+
+    DividerSnapAlgorithm getSnapAlgorithm() {
+        if (mSnapAlgorithm == null) {
+            updateResources();
+            boolean isHorizontalDivision = !mDisplayLayout.isLandscape();
+            mSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(),
+                    mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize,
+                    isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide());
+        }
+        return mSnapAlgorithm;
+    }
+
+    DividerSnapAlgorithm getMinimizedSnapAlgorithm() {
+        if (mMinimizedSnapAlgorithm == null) {
+            updateResources();
+            boolean isHorizontalDivision = !mDisplayLayout.isLandscape();
+            mMinimizedSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(),
+                    mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize,
+                    isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide(),
+                    true /* isMinimized */);
+        }
+        return mMinimizedSnapAlgorithm;
+    }
+
+    void resizeSplits(int position) {
+        mPrimary = mPrimary == null ? new Rect() : mPrimary;
+        mSecondary = mSecondary == null ? new Rect() : mSecondary;
+        calcSplitBounds(position, mPrimary, mSecondary);
+    }
+
+    void resizeSplits(int position, WindowContainerTransaction t) {
+        resizeSplits(position);
+        t.setBounds(mTiles.mPrimary.token, mPrimary);
+        t.setBounds(mTiles.mSecondary.token, mSecondary);
+
+        t.setSmallestScreenWidthDp(mTiles.mPrimary.token,
+                getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary));
+        t.setSmallestScreenWidthDp(mTiles.mSecondary.token,
+                getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary));
+    }
+
+    void calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary) {
+        int dockSide = getPrimarySplitSide();
+        DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outPrimary,
+                mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize);
+
+        DockedDividerUtils.calculateBoundsForPosition(position,
+                DockedDividerUtils.invertDockSide(dockSide), outSecondary, mDisplayLayout.width(),
+                mDisplayLayout.height(), mDividerSize);
+    }
+
+    Rect calcMinimizedHomeStackBounds() {
+        DividerSnapAlgorithm.SnapTarget miniMid = getMinimizedSnapAlgorithm().getMiddleTarget();
+        Rect homeBounds = new Rect();
+        DockedDividerUtils.calculateBoundsForPosition(miniMid.position,
+                DockedDividerUtils.invertDockSide(getPrimarySplitSide()), homeBounds,
+                mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize);
+        return homeBounds;
+    }
+
+    /**
+     * Updates the adjustment depending on it's current state.
+     */
+    void updateAdjustedBounds(int currImeTop, int startTop, int finalTop) {
+        updateAdjustedBounds(mDisplayLayout, currImeTop, startTop, finalTop, mDividerSize,
+                mDividerSizeInactive, mPrimary, mSecondary);
+    }
+
+    /**
+     * Updates the adjustment depending on it's current state.
+     */
+    private void updateAdjustedBounds(DisplayLayout dl, int currImeTop, int startTop, int finalTop,
+            int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds) {
+        adjustForIME(dl, currImeTop, startTop, finalTop, dividerWidth, dividerWidthInactive,
+                primaryBounds, secondaryBounds);
+    }
+
+    /** Assumes top/bottom split. Splits are not adjusted for left/right splits. */
+    private void adjustForIME(DisplayLayout dl, int currImeTop, int startTop, int finalTop,
+            int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds) {
+        if (mAdjustedPrimary == null) {
+            mAdjustedPrimary = new Rect();
+            mAdjustedSecondary = new Rect();
+        }
+
+        final Rect displayStableRect = new Rect();
+        dl.getStableBounds(displayStableRect);
+
+        final boolean showing = finalTop < startTop;
+        final float progress = ((float) (currImeTop - startTop)) / (finalTop - startTop);
+        final float dividerSquish = showing ? progress : 1.f - progress;
+        final int currDividerWidth =
+                (int) (dividerWidthInactive * dividerSquish + dividerWidth * (1.f - dividerSquish));
+
+        final int minTopStackBottom = displayStableRect.top
+                + (int) ((mPrimary.bottom - displayStableRect.top) * ADJUSTED_STACK_FRACTION_MIN);
+        final int minImeTop = minTopStackBottom + currDividerWidth;
+
+        // Calculate an offset which shifts the stacks up by the height of the IME, but still
+        // leaves at least 30% of the top stack visible.
+        final int yOffset = Math.max(0, dl.height() - Math.max(currImeTop, minImeTop));
+
+        // TOP
+        // Reduce the offset by an additional small amount to squish the divider bar.
+        mAdjustedPrimary.set(primaryBounds);
+        mAdjustedPrimary.offset(0, -yOffset + (dividerWidth - currDividerWidth));
+
+        // BOTTOM
+        mAdjustedSecondary.set(secondaryBounds);
+        mAdjustedSecondary.offset(0, -yOffset);
+    }
+
+    static int getSmallestWidthDpForBounds(@NonNull Context context, DisplayLayout dl,
+            Rect bounds) {
+        int dividerSize = DockedDividerUtils.getDividerSize(context.getResources(),
+                DockedDividerUtils.getDividerInsets(context.getResources()));
+
+        int minWidth = Integer.MAX_VALUE;
+
+        // Go through all screen orientations and find the orientation in which the task has the
+        // smallest width.
+        Rect tmpRect = new Rect();
+        Rect rotatedDisplayRect = new Rect();
+        Rect displayRect = new Rect(0, 0, dl.width(), dl.height());
+
+        DisplayLayout tmpDL = new DisplayLayout();
+        for (int rotation = 0; rotation < 4; rotation++) {
+            tmpDL.set(dl);
+            tmpDL.rotateTo(context.getResources(), rotation);
+            DividerSnapAlgorithm snap = initSnapAlgorithmForRotation(context, tmpDL, dividerSize);
+
+            tmpRect.set(bounds);
+            DisplayLayout.rotateBounds(tmpRect, displayRect, rotation - dl.rotation());
+            rotatedDisplayRect.set(0, 0, tmpDL.width(), tmpDL.height());
+            final int dockSide = getPrimarySplitSide(tmpRect, rotatedDisplayRect,
+                    tmpDL.getOrientation());
+            final int position = DockedDividerUtils.calculatePositionForBounds(tmpRect, dockSide,
+                    dividerSize);
+
+            final int snappedPosition =
+                    snap.calculateNonDismissingSnapTarget(position).position;
+            DockedDividerUtils.calculateBoundsForPosition(snappedPosition, dockSide, tmpRect,
+                    tmpDL.width(), tmpDL.height(), dividerSize);
+            Rect insettedDisplay = new Rect(rotatedDisplayRect);
+            insettedDisplay.inset(tmpDL.stableInsets());
+            tmpRect.intersect(insettedDisplay);
+            minWidth = Math.min(tmpRect.width(), minWidth);
+        }
+        return (int) (minWidth / dl.density());
+    }
+
+    static DividerSnapAlgorithm initSnapAlgorithmForRotation(Context context, DisplayLayout dl,
+            int dividerSize) {
+        final Configuration config = new Configuration();
+        config.unset();
+        config.orientation = dl.getOrientation();
+        Rect tmpRect = new Rect(0, 0, dl.width(), dl.height());
+        tmpRect.inset(dl.nonDecorInsets());
+        config.windowConfiguration.setAppBounds(tmpRect);
+        tmpRect.set(0, 0, dl.width(), dl.height());
+        tmpRect.inset(dl.stableInsets());
+        config.screenWidthDp = (int) (tmpRect.width() / dl.density());
+        config.screenHeightDp = (int) (tmpRect.height() / dl.density());
+        final Context rotationContext = context.createConfigurationContext(config);
+        return new DividerSnapAlgorithm(
+                rotationContext.getResources(), dl.width(), dl.height(), dividerSize,
+                config.orientation == ORIENTATION_PORTRAIT, dl.stableInsets());
+    }
+
+    /**
+     * Get the current primary-split side. Determined by its location of {@param bounds} within
+     * {@param displayRect} but if both are the same, it will try to dock to each side and determine
+     * if allowed in its respected {@param orientation}.
+     *
+     * @param bounds bounds of the primary split task to get which side is docked
+     * @param displayRect bounds of the display that contains the primary split task
+     * @param orientation the origination of device
+     * @return current primary-split side
+     */
+    static int getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation) {
+        if (orientation == ORIENTATION_PORTRAIT) {
+            // Portrait mode, docked either at the top or the bottom.
+            final int diff = (displayRect.bottom - bounds.bottom) - (bounds.top - displayRect.top);
+            if (diff < 0) {
+                return DOCKED_BOTTOM;
+            } else {
+                // Top is default
+                return DOCKED_TOP;
+            }
+        } else if (orientation == ORIENTATION_LANDSCAPE) {
+            // Landscape mode, docked either on the left or on the right.
+            final int diff = (displayRect.right - bounds.right) - (bounds.left - displayRect.left);
+            if (diff < 0) {
+                return DOCKED_RIGHT;
+            }
+            return DOCKED_LEFT;
+        }
+        return DOCKED_INVALID;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java
new file mode 100644
index 0000000..5cc8799
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java
@@ -0,0 +1,159 @@
+/*
+ * 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.systemui.stackdivider;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ITaskOrganizerController;
+import android.app.WindowConfiguration;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.Display;
+import android.view.ITaskOrganizer;
+import android.view.IWindowContainer;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+
+class SplitScreenTaskOrganizer extends ITaskOrganizer.Stub {
+    private static final String TAG = "SplitScreenTaskOrganizer";
+    private static final boolean DEBUG = Divider.DEBUG;
+
+    RunningTaskInfo mPrimary;
+    RunningTaskInfo mSecondary;
+    SurfaceControl mPrimarySurface;
+    SurfaceControl mSecondarySurface;
+    SurfaceControl mPrimaryDim;
+    SurfaceControl mSecondaryDim;
+    final Divider mDivider;
+
+    SplitScreenTaskOrganizer(Divider divider) {
+        mDivider = divider;
+    }
+
+    void init(ITaskOrganizerController organizerController, SurfaceSession session)
+            throws RemoteException {
+        organizerController.registerTaskOrganizer(this, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+        organizerController.registerTaskOrganizer(this, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+        mPrimary = organizerController.createRootTask(Display.DEFAULT_DISPLAY,
+                WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+        mSecondary = organizerController.createRootTask(Display.DEFAULT_DISPLAY,
+                WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+        mPrimarySurface = mPrimary.token.getLeash();
+        mSecondarySurface = mSecondary.token.getLeash();
+
+        // Initialize dim surfaces:
+        mPrimaryDim = new SurfaceControl.Builder(session).setParent(mPrimarySurface)
+                .setColorLayer().setName("Primary Divider Dim").build();
+        mSecondaryDim = new SurfaceControl.Builder(session).setParent(mSecondarySurface)
+                .setColorLayer().setName("Secondary Divider Dim").build();
+        SurfaceControl.Transaction t = getTransaction();
+        t.setLayer(mPrimaryDim, Integer.MAX_VALUE);
+        t.setColor(mPrimaryDim, new float[]{0f, 0f, 0f});
+        t.setLayer(mSecondaryDim, Integer.MAX_VALUE);
+        t.setColor(mSecondaryDim, new float[]{0f, 0f, 0f});
+        t.apply();
+        releaseTransaction(t);
+    }
+
+    SurfaceControl.Transaction getTransaction() {
+        return mDivider.mTransactionPool.acquire();
+    }
+
+    void releaseTransaction(SurfaceControl.Transaction t) {
+        mDivider.mTransactionPool.release(t);
+    }
+
+    @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 taskInfo) {
+        if (taskInfo.displayId != DEFAULT_DISPLAY) {
+            return;
+        }
+        mDivider.getHandler().post(() -> handleTaskInfoChanged(taskInfo));
+    }
+
+    /**
+     * This is effectively a finite state machine which moves between the various split-screen
+     * presentations based on the contents of the split regions.
+     */
+    private void handleTaskInfoChanged(RunningTaskInfo info) {
+        final boolean primaryWasEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
+        final boolean secondaryWasEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
+        if (info.token.asBinder() == mPrimary.token.asBinder()) {
+            mPrimary = info;
+        } else if (info.token.asBinder() == mSecondary.token.asBinder()) {
+            mSecondary = info;
+        }
+        final boolean primaryIsEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
+        final boolean secondaryIsEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
+        if (DEBUG) {
+            Log.d(TAG, "onTaskInfoChanged " + mPrimary + "  " + mSecondary);
+        }
+        if (primaryIsEmpty || secondaryIsEmpty) {
+            // At-least one of the splits is empty which means we are currently transitioning
+            // into or out-of split-screen mode.
+            if (DEBUG) {
+                Log.d(TAG, " at-least one split empty " + mPrimary.topActivityType
+                        + "  " + mSecondary.topActivityType);
+            }
+            if (mDivider.inSplitMode()) {
+                // Was in split-mode, which means we are leaving split, so continue that.
+                // This happens when the stack in the primary-split is dismissed.
+                if (DEBUG) {
+                    Log.d(TAG, "    was in split, so this means leave it "
+                            + mPrimary.topActivityType + "  " + mSecondary.topActivityType);
+                }
+                WindowManagerProxy.applyDismissSplit(this, true /* dismissOrMaximize */);
+                mDivider.updateVisibility(false /* visible */);
+            } else if (!primaryIsEmpty && primaryWasEmpty && secondaryWasEmpty) {
+                // Wasn't in split-mode (both were empty), but now that the primary split is
+                // populated, we should fully enter split by moving everything else into secondary.
+                // This just tells window-manager to reparent things, the UI will respond
+                // when it gets new task info for the secondary split.
+                if (DEBUG) {
+                    Log.d(TAG, "   was not in split, but primary is populated, so enter it");
+                }
+                mDivider.startEnterSplit();
+            }
+        } else if (mSecondary.topActivityType == ACTIVITY_TYPE_HOME
+                || mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS) {
+            // Both splits are populated but the secondary split has a home/recents stack on top,
+            // so enter minimized mode.
+            mDivider.ensureMinimizedSplit();
+        } else {
+            // Both splits are populated by normal activities, so make sure we aren't minimized.
+            mDivider.ensureNormalSplit();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
index 228aab5..7685733 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
@@ -16,16 +16,25 @@
 
 package com.android.systemui.stackdivider;
 
-import static android.view.WindowManager.DOCKED_INVALID;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Display.DEFAULT_DISPLAY;
 
+import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.graphics.Rect;
 import android.os.RemoteException;
 import android.util.Log;
+import android.view.Display;
+import android.view.IWindowContainer;
+import android.view.WindowContainerTransaction;
 import android.view.WindowManagerGlobal;
 
 import com.android.internal.annotations.GuardedBy;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -35,88 +44,20 @@
 public class WindowManagerProxy {
 
     private static final String TAG = "WindowManagerProxy";
+    private static final int[] HOME_AND_RECENTS = {ACTIVITY_TYPE_HOME, ACTIVITY_TYPE_RECENTS};
 
     private static final WindowManagerProxy sInstance = new WindowManagerProxy();
 
     @GuardedBy("mDockedRect")
     private final Rect mDockedRect = new Rect();
-    private final Rect mTempDockedTaskRect = new Rect();
-    private final Rect mTempDockedInsetRect = new Rect();
-    private final Rect mTempOtherTaskRect = new Rect();
-    private final Rect mTempOtherInsetRect = new Rect();
 
     private final Rect mTmpRect1 = new Rect();
-    private final Rect mTmpRect2 = new Rect();
-    private final Rect mTmpRect3 = new Rect();
-    private final Rect mTmpRect4 = new Rect();
-    private final Rect mTmpRect5 = new Rect();
 
     @GuardedBy("mDockedRect")
     private final Rect mTouchableRegion = new Rect();
 
-    private boolean mDimLayerVisible;
-    private int mDimLayerTargetWindowingMode;
-    private float mDimLayerAlpha;
-
     private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
 
-    private final Runnable mResizeRunnable = new Runnable() {
-        @Override
-        public void run() {
-            synchronized (mDockedRect) {
-                mTmpRect1.set(mDockedRect);
-                mTmpRect2.set(mTempDockedTaskRect);
-                mTmpRect3.set(mTempDockedInsetRect);
-                mTmpRect4.set(mTempOtherTaskRect);
-                mTmpRect5.set(mTempOtherInsetRect);
-            }
-            try {
-                ActivityTaskManager.getService()
-                        .resizeDockedStack(mTmpRect1,
-                                mTmpRect2.isEmpty() ? null : mTmpRect2,
-                                mTmpRect3.isEmpty() ? null : mTmpRect3,
-                                mTmpRect4.isEmpty() ? null : mTmpRect4,
-                                mTmpRect5.isEmpty() ? null : mTmpRect5);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed to resize stack: " + e);
-            }
-        }
-    };
-
-    private final Runnable mDismissRunnable = new Runnable() {
-        @Override
-        public void run() {
-            try {
-                ActivityTaskManager.getService().dismissSplitScreenMode(false /* onTop */);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed to remove stack: " + e);
-            }
-        }
-    };
-
-    private final Runnable mMaximizeRunnable = new Runnable() {
-        @Override
-        public void run() {
-            try {
-                ActivityTaskManager.getService().dismissSplitScreenMode(true /* onTop */);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed to resize stack: " + e);
-            }
-        }
-    };
-
-    private final Runnable mDimLayerRunnable = new Runnable() {
-        @Override
-        public void run() {
-            try {
-                WindowManagerGlobal.getWindowManagerService().setResizeDimLayer(mDimLayerVisible,
-                        mDimLayerTargetWindowingMode, mDimLayerAlpha);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed to resize stack: " + e);
-            }
-        }
-    };
-
     private final Runnable mSetTouchableRegionRunnable = new Runnable() {
         @Override
         public void run() {
@@ -139,40 +80,9 @@
         return sInstance;
     }
 
-    public void resizeDockedStack(Rect docked, Rect tempDockedTaskRect, Rect tempDockedInsetRect,
-            Rect tempOtherTaskRect, Rect tempOtherInsetRect) {
-        synchronized (mDockedRect) {
-            mDockedRect.set(docked);
-            if (tempDockedTaskRect != null) {
-                mTempDockedTaskRect.set(tempDockedTaskRect);
-            } else {
-                mTempDockedTaskRect.setEmpty();
-            }
-            if (tempDockedInsetRect != null) {
-                mTempDockedInsetRect.set(tempDockedInsetRect);
-            } else {
-                mTempDockedInsetRect.setEmpty();
-            }
-            if (tempOtherTaskRect != null) {
-                mTempOtherTaskRect.set(tempOtherTaskRect);
-            } else {
-                mTempOtherTaskRect.setEmpty();
-            }
-            if (tempOtherInsetRect != null) {
-                mTempOtherInsetRect.set(tempOtherInsetRect);
-            } else {
-                mTempOtherInsetRect.setEmpty();
-            }
-        }
-        mExecutor.execute(mResizeRunnable);
-    }
-
-    public void dismissDockedStack() {
-        mExecutor.execute(mDismissRunnable);
-    }
-
-    public void maximizeDockedStack() {
-        mExecutor.execute(mMaximizeRunnable);
+    void dismissOrMaximizeDocked(
+            final SplitScreenTaskOrganizer tiles, final boolean dismissOrMaximize) {
+        mExecutor.execute(() -> applyDismissSplit(tiles, dismissOrMaximize));
     }
 
     public void setResizing(final boolean resizing) {
@@ -188,26 +98,204 @@
         });
     }
 
-    public int getDockSide() {
-        try {
-            return WindowManagerGlobal.getWindowManagerService().getDockedStackSide();
-        } catch (RemoteException e) {
-            Log.w(TAG, "Failed to get dock side: " + e);
-        }
-        return DOCKED_INVALID;
-    }
-
-    public void setResizeDimLayer(boolean visible, int targetWindowingMode, float alpha) {
-        mDimLayerVisible = visible;
-        mDimLayerTargetWindowingMode = targetWindowingMode;
-        mDimLayerAlpha = alpha;
-        mExecutor.execute(mDimLayerRunnable);
-    }
-
+    /** Sets a touch region */
     public void setTouchRegion(Rect region) {
         synchronized (mDockedRect) {
             mTouchableRegion.set(region);
         }
         mExecutor.execute(mSetTouchableRegionRunnable);
     }
+
+    static void applyResizeSplits(int position, SplitDisplayLayout splitLayout) {
+        WindowContainerTransaction t = new WindowContainerTransaction();
+        splitLayout.resizeSplits(position, t);
+        try {
+            ActivityTaskManager.getTaskOrganizerController().applyContainerTransaction(t,
+                    null /* organizer */);
+        } catch (RemoteException e) {
+        }
+    }
+
+    private static boolean getHomeAndRecentsTasks(List<IWindowContainer> out,
+            IWindowContainer parent) {
+        boolean resizable = false;
+        try {
+            List<ActivityManager.RunningTaskInfo> rootTasks = parent == null
+                    ? ActivityTaskManager.getTaskOrganizerController().getRootTasks(
+                            Display.DEFAULT_DISPLAY, HOME_AND_RECENTS)
+                    : ActivityTaskManager.getTaskOrganizerController().getChildTasks(parent,
+                            HOME_AND_RECENTS);
+            for (int i = 0, n = rootTasks.size(); i < n; ++i) {
+                final ActivityManager.RunningTaskInfo ti = rootTasks.get(i);
+                out.add(ti.token);
+                if (ti.topActivityType == ACTIVITY_TYPE_HOME) {
+                    resizable = ti.isResizable();
+                }
+            }
+        } catch (RemoteException e) {
+        }
+        return resizable;
+    }
+
+    static void applyHomeTasksMinimized(SplitDisplayLayout layout, IWindowContainer parent) {
+        applyHomeTasksMinimized(layout, parent, null /* transaction */);
+    }
+
+    /**
+     * Assign a fixed override-bounds to home tasks that reflect their geometry while the primary
+     * split is minimized. This actually "sticks out" of the secondary split area, but when in
+     * minimized mode, the secondary split gets a 'negative' crop to expose it.
+     */
+    static boolean applyHomeTasksMinimized(SplitDisplayLayout layout, IWindowContainer parent,
+            WindowContainerTransaction t) {
+        // Resize the home/recents stacks to the larger minimized-state size
+        final Rect homeBounds;
+        final ArrayList<IWindowContainer> homeStacks = new ArrayList<>();
+        boolean isHomeResizable = getHomeAndRecentsTasks(homeStacks, parent);
+        if (isHomeResizable) {
+            homeBounds = layout.calcMinimizedHomeStackBounds();
+        } else {
+            homeBounds = new Rect(0, 0, layout.mDisplayLayout.width(),
+                    layout.mDisplayLayout.height());
+        }
+        WindowContainerTransaction wct = t != null ? t : new WindowContainerTransaction();
+        for (int i = homeStacks.size() - 1; i >= 0; --i) {
+            wct.setBounds(homeStacks.get(i), homeBounds);
+        }
+        if (t != null) {
+            return isHomeResizable;
+        }
+        try {
+            ActivityTaskManager.getTaskOrganizerController().applyContainerTransaction(wct,
+                    null /* organizer */);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to resize home stacks ", e);
+        }
+        return isHomeResizable;
+    }
+
+    /**
+     * Finishes entering split-screen by reparenting all FULLSCREEN tasks into the secondary split.
+     * This assumes there is already something in the primary split since that is usually what
+     * triggers a call to this. In the same transaction, this overrides the home task bounds via
+     * {@link #applyHomeTasksMinimized}.
+     *
+     * @return whether the home stack is resizable
+     */
+    static boolean applyEnterSplit(SplitScreenTaskOrganizer tiles, SplitDisplayLayout layout) {
+        try {
+            // Set launchtile first so that any stack created after
+            // getAllStackInfos and before reparent (even if unlikely) are placed
+            // correctly.
+            ActivityTaskManager.getTaskOrganizerController().setLaunchRoot(
+                    DEFAULT_DISPLAY, tiles.mSecondary.token);
+            List<ActivityManager.RunningTaskInfo> rootTasks =
+                    ActivityTaskManager.getTaskOrganizerController().getRootTasks(DEFAULT_DISPLAY,
+                            null /* activityTypes */);
+            WindowContainerTransaction wct = new WindowContainerTransaction();
+            if (rootTasks.isEmpty()) {
+                return false;
+            }
+            for (int i = rootTasks.size() - 1; i >= 0; --i) {
+                if (rootTasks.get(i).configuration.windowConfiguration.getWindowingMode()
+                        != WINDOWING_MODE_FULLSCREEN) {
+                    continue;
+                }
+                wct.reparent(rootTasks.get(i).token, tiles.mSecondary.token,
+                        true /* onTop */);
+            }
+            boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, wct);
+            ActivityTaskManager.getTaskOrganizerController()
+                    .applyContainerTransaction(wct, null /* organizer */);
+            return isHomeResizable;
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error moving fullscreen tasks to secondary split: " + e);
+        }
+        return false;
+    }
+
+    private static boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) {
+        final int atype = ti.configuration.windowConfiguration.getActivityType();
+        return atype == ACTIVITY_TYPE_HOME || atype == ACTIVITY_TYPE_RECENTS;
+    }
+
+    /**
+     * Reparents all tile members back to their display and resets home task override bounds.
+     * @param dismissOrMaximize When {@code true} this resolves the split by closing the primary
+     *                          split (thus resulting in the top of the secondary split becoming
+     *                          fullscreen. {@code false} resolves the other way.
+     */
+    static void applyDismissSplit(SplitScreenTaskOrganizer tiles, boolean dismissOrMaximize) {
+        try {
+            // Set launch root first so that any task created after getChildContainers and
+            // before reparent (pretty unlikely) are put into fullscreen.
+            ActivityTaskManager.getTaskOrganizerController().setLaunchRoot(Display.DEFAULT_DISPLAY,
+                    null);
+            // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished
+            //                 plus specific APIs to clean this up.
+            List<ActivityManager.RunningTaskInfo> primaryChildren =
+                    ActivityTaskManager.getTaskOrganizerController().getChildTasks(
+                            tiles.mPrimary.token, null /* activityTypes */);
+            List<ActivityManager.RunningTaskInfo> secondaryChildren =
+                    ActivityTaskManager.getTaskOrganizerController().getChildTasks(
+                            tiles.mSecondary.token, null /* activityTypes */);
+            // In some cases (eg. non-resizable is launched), system-server will leave split-screen.
+            // as a result, the above will not capture any tasks; yet, we need to clean-up the
+            // home task bounds.
+            List<ActivityManager.RunningTaskInfo> freeHomeAndRecents =
+                    ActivityTaskManager.getTaskOrganizerController().getRootTasks(
+                            Display.DEFAULT_DISPLAY, HOME_AND_RECENTS);
+            if (primaryChildren.isEmpty() && secondaryChildren.isEmpty()
+                    && freeHomeAndRecents.isEmpty()) {
+                return;
+            }
+            WindowContainerTransaction wct = new WindowContainerTransaction();
+            if (dismissOrMaximize) {
+                // Dismissing, so move all primary split tasks first
+                for (int i = primaryChildren.size() - 1; i >= 0; --i) {
+                    wct.reparent(primaryChildren.get(i).token, null /* parent */,
+                            true /* onTop */);
+                }
+                // Don't need to worry about home tasks because they are already in the "proper"
+                // order within the secondary split.
+                for (int i = secondaryChildren.size() - 1; i >= 0; --i) {
+                    final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i);
+                    wct.reparent(ti.token, null /* parent */, true /* onTop */);
+                    if (isHomeOrRecentTask(ti)) {
+                        wct.setBounds(ti.token, null);
+                    }
+                }
+            } else {
+                // Maximize, so move non-home secondary split first
+                for (int i = secondaryChildren.size() - 1; i >= 0; --i) {
+                    if (isHomeOrRecentTask(secondaryChildren.get(i))) {
+                        continue;
+                    }
+                    wct.reparent(secondaryChildren.get(i).token, null /* parent */,
+                            true /* onTop */);
+                }
+                // Find and place home tasks in-between. This simulates the fact that there was
+                // nothing behind the primary split's tasks.
+                for (int i = secondaryChildren.size() - 1; i >= 0; --i) {
+                    final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i);
+                    if (isHomeOrRecentTask(ti)) {
+                        wct.reparent(ti.token, null /* parent */, true /* onTop */);
+                        // reset bounds too
+                        wct.setBounds(ti.token, null);
+                    }
+                }
+                for (int i = primaryChildren.size() - 1; i >= 0; --i) {
+                    wct.reparent(primaryChildren.get(i).token, null /* parent */,
+                            true /* onTop */);
+                }
+            }
+            for (int i = freeHomeAndRecents.size() - 1; i >= 0; --i) {
+                wct.setBounds(freeHomeAndRecents.get(i).token, null);
+            }
+            ActivityTaskManager.getTaskOrganizerController().applyContainerTransaction(wct,
+                    null /* organizer */);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to remove stack: " + e);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
index 93f5805..55a20fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
@@ -51,10 +51,10 @@
 import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.systemui.Dependency;
-import com.android.systemui.DockedStackExistsListener;
 import com.android.systemui.R;
 import com.android.systemui.SystemUI;
 import com.android.systemui.dagger.qualifiers.UiBackground;
+import com.android.systemui.stackdivider.Divider;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.NotificationChannels;
@@ -80,11 +80,13 @@
     private final CommandQueue mCommandQueue;
     private boolean mDockedStackExists;
     private KeyguardStateController mKeyguardStateController;
+    private final Divider mDivider;
 
     @Inject
     public InstantAppNotifier(Context context, CommandQueue commandQueue,
-            @UiBackground Executor uiBgExecutor) {
+            @UiBackground Executor uiBgExecutor, Divider divider) {
         super(context);
+        mDivider = divider;
         mCommandQueue = commandQueue;
         mUiBgExecutor = uiBgExecutor;
     }
@@ -103,7 +105,7 @@
         mCommandQueue.addCallback(this);
         mKeyguardStateController.addCallback(this);
 
-        DockedStackExistsListener.register(
+        mDivider.registerInSplitScreenListener(
                 exists -> {
                     mDockedStackExists = exists;
                     updateForegroundInstantApps();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index d790cbc..84aecd4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -63,7 +63,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.Dependency;
-import com.android.systemui.DockedStackExistsListener;
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 import com.android.systemui.assist.AssistHandleViewController;
@@ -75,6 +74,7 @@
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.WindowManagerWrapper;
+import com.android.systemui.stackdivider.Divider;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NavigationBarController;
 import com.android.systemui.statusbar.policy.DeadZone;
@@ -869,7 +869,8 @@
 
         getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener);
 
-        DockedStackExistsListener.register(mDockedListener);
+        Divider divider = Dependency.get(Divider.class);
+        divider.registerInSplitScreenListener(mDockedListener);
         updateOrientationViews();
         reloadNavIcons();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 301eac2..6298afe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -163,7 +163,6 @@
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 import com.android.systemui.stackdivider.Divider;
-import com.android.systemui.stackdivider.WindowManagerProxy;
 import com.android.systemui.statusbar.BackDropView;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.CrossFadeHelper;
@@ -1410,8 +1409,11 @@
         if (!mRecentsOptional.isPresent()) {
             return false;
         }
-        int dockSide = WindowManagerProxy.getInstance().getDockSide();
-        if (dockSide == WindowManager.DOCKED_INVALID) {
+        Divider divider = null;
+        if (mDividerOptional.isPresent()) {
+            divider = mDividerOptional.get();
+        }
+        if (divider == null || !divider.inSplitMode()) {
             final int navbarPos = WindowManagerWrapper.getInstance().getNavBarPosition(mDisplayId);
             if (navbarPos == NAV_BAR_POS_INVALID) {
                 return false;
@@ -1421,16 +1423,13 @@
                     : SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
             return mRecentsOptional.get().splitPrimaryTask(createMode, null, metricsDockAction);
         } else {
-            if (mDividerOptional.isPresent()) {
-                Divider divider = mDividerOptional.get();
-                if (divider.isMinimized() && !divider.isHomeStackResizable()) {
-                    // Undocking from the minimized state is not supported
-                    return false;
-                } else {
-                    divider.onUndockingTask();
-                    if (metricsUndockAction != -1) {
-                        mMetricsLogger.action(metricsUndockAction);
-                    }
+            if (divider.isMinimized() && !divider.isHomeStackResizable()) {
+                // Undocking from the minimized state is not supported
+                return false;
+            } else {
+                divider.onUndockingTask();
+                if (metricsUndockAction != -1) {
+                    mMetricsLogger.action(metricsUndockAction);
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/wm/DisplayController.java b/packages/SystemUI/src/com/android/systemui/wm/DisplayController.java
index bc24ad0..c66f07d 100644
--- a/packages/SystemUI/src/com/android/systemui/wm/DisplayController.java
+++ b/packages/SystemUI/src/com/android/systemui/wm/DisplayController.java
@@ -101,6 +101,11 @@
                                 return;
                             }
                             Display display = getDisplay(displayId);
+                            if (display == null) {
+                                Slog.w(TAG, "Skipping Display Configuration change on invalid"
+                                        + " display. It may have been removed.");
+                                return;
+                            }
                             Context perDisplayContext = mContext;
                             if (displayId != Display.DEFAULT_DISPLAY) {
                                 perDisplayContext = mContext.createDisplayContext(display);
diff --git a/packages/SystemUI/src/com/android/systemui/wm/DisplayImeController.java b/packages/SystemUI/src/com/android/systemui/wm/DisplayImeController.java
index 7dad05d..1b62cbf 100644
--- a/packages/SystemUI/src/com/android/systemui/wm/DisplayImeController.java
+++ b/packages/SystemUI/src/com/android/systemui/wm/DisplayImeController.java
@@ -34,6 +34,7 @@
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
 
+import com.android.systemui.TransactionPool;
 import com.android.systemui.dagger.qualifiers.Main;
 
 import java.util.ArrayList;
@@ -48,8 +49,8 @@
 public class DisplayImeController implements DisplayController.OnDisplaysChangedListener {
     private static final String TAG = "DisplayImeController";
 
-    static final int ANIMATION_DURATION_SHOW_MS = 275;
-    static final int ANIMATION_DURATION_HIDE_MS = 340;
+    public static final int ANIMATION_DURATION_SHOW_MS = 275;
+    public static final int ANIMATION_DURATION_HIDE_MS = 340;
     static final Interpolator INTERPOLATOR = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
     private static final int DIRECTION_NONE = 0;
     private static final int DIRECTION_SHOW = 1;
@@ -57,6 +58,7 @@
 
     SystemWindows mSystemWindows;
     final Handler mHandler;
+    final TransactionPool mTransactionPool;
 
     final SparseArray<PerDisplay> mImePerDisplay = new SparseArray<>();
 
@@ -64,9 +66,10 @@
 
     @Inject
     public DisplayImeController(SystemWindows syswin, DisplayController displayController,
-            @Main Handler mainHandler) {
+            @Main Handler mainHandler, TransactionPool transactionPool) {
         mHandler = mainHandler;
         mSystemWindows = syswin;
+        mTransactionPool = transactionPool;
         displayController.addDisplayWindowListener(this);
     }
 
@@ -255,18 +258,18 @@
                         show ? ANIMATION_DURATION_SHOW_MS : ANIMATION_DURATION_HIDE_MS);
 
                 mAnimation.addUpdateListener(animation -> {
-                    SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+                    SurfaceControl.Transaction t = mTransactionPool.acquire();
                     float value = (float) animation.getAnimatedValue();
                     t.setPosition(mImeSourceControl.getLeash(), x, value);
                     dispatchPositionChanged(mDisplayId, imeTop(imeSource, value), t);
                     t.apply();
-                    t.close();
+                    mTransactionPool.release(t);
                 });
                 mAnimation.setInterpolator(INTERPOLATOR);
                 mAnimation.addListener(new AnimatorListenerAdapter() {
                     @Override
                     public void onAnimationStart(Animator animation) {
-                        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+                        SurfaceControl.Transaction t = mTransactionPool.acquire();
                         t.setPosition(mImeSourceControl.getLeash(), x, startY);
                         dispatchStartPositioning(mDisplayId, imeTop(imeSource, startY),
                                 imeTop(imeSource, endY), mAnimationDirection == DIRECTION_SHOW,
@@ -275,11 +278,11 @@
                             t.show(mImeSourceControl.getLeash());
                         }
                         t.apply();
-                        t.close();
+                        mTransactionPool.release(t);
                     }
                     @Override
                     public void onAnimationEnd(Animator animation) {
-                        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+                        SurfaceControl.Transaction t = mTransactionPool.acquire();
                         t.setPosition(mImeSourceControl.getLeash(), x, endY);
                         dispatchEndPositioning(mDisplayId, imeTop(imeSource, endY),
                                 mAnimationDirection == DIRECTION_SHOW, t);
@@ -287,7 +290,7 @@
                             t.hide(mImeSourceControl.getLeash());
                         }
                         t.apply();
-                        t.close();
+                        mTransactionPool.release(t);
 
                         mAnimationDirection = DIRECTION_NONE;
                         mAnimation = null;
diff --git a/packages/SystemUI/src/com/android/systemui/wm/DisplayLayout.java b/packages/SystemUI/src/com/android/systemui/wm/DisplayLayout.java
index 64b0b66..4652abf 100644
--- a/packages/SystemUI/src/com/android/systemui/wm/DisplayLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/wm/DisplayLayout.java
@@ -35,6 +35,7 @@
 import android.graphics.Rect;
 import android.os.SystemProperties;
 import android.provider.Settings;
+import android.util.DisplayMetrics;
 import android.util.RotationUtils;
 import android.util.Size;
 import android.view.Display;
@@ -191,6 +192,11 @@
         return mDensityDpi;
     }
 
+    /** Get the density scale for the display. */
+    public float density() {
+        return mDensityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
+    }
+
     /** Get whether this layout is landscape. */
     public boolean isLandscape() {
         return mWidth > mHeight;
diff --git a/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java b/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java
index 044a2a6..23df991 100644
--- a/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java
+++ b/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java
@@ -162,6 +162,23 @@
         return pd.getWindow(windowType);
     }
 
+    /**
+     * Gets the SurfaceControl associated with a root view. This is the same surface that backs the
+     * ViewRootImpl.
+     */
+    public SurfaceControl getViewSurface(View rootView) {
+        for (int i = 0; i < mPerDisplay.size(); ++i) {
+            for (int iWm = 0; iWm < mPerDisplay.valueAt(i).mWwms.size(); ++iWm) {
+                SurfaceControl out =
+                        mPerDisplay.valueAt(i).mWwms.get(iWm).getSurfaceControlForWindow(rootView);
+                if (out != null) {
+                    return out;
+                }
+            }
+        }
+        return null;
+    }
+
     private class PerDisplay {
         final int mDisplayId;
         private final SparseArray<SysUiWindowManager> mWwms = new SparseArray<>();
@@ -256,6 +273,10 @@
         void updateConfiguration(Configuration configuration) {
             setConfiguration(configuration);
         }
+
+        SurfaceControl getSurfaceControlForWindow(View rootView) {
+            return getSurfaceControl(rootView);
+        }
     }
 
     class ContainerWindow extends IWindow.Stub {