Merge "Fixes AttentionManagerServiceTest."
diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING
index f4645ca..39c196d 100644
--- a/core/java/android/os/TEST_MAPPING
+++ b/core/java/android/os/TEST_MAPPING
@@ -1,6 +1,14 @@
 {
   "presubmit": [
-    // TODO(159590499) add BugreportManagerTestCases
+    {
+      "file_patterns": ["Bugreport[^/]*\\.java"],
+      "name": "BugreportManagerTestCases",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        }
+      ]
+    },
     {
       "file_patterns": ["Bugreport[^/]*\\.java"],
       "name": "CtsBugreportTestCases",
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index bd7e610..cfe7edd 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10855,17 +10855,6 @@
        public static final String MODE_RINGER = "mode_ringer";
 
         /**
-         * Specifies whether Enhanced Connectivity is enabled or not. This setting allows the
-         * Connectivity Thermal Power Manager to actively help the device to save power in 5G
-         * scenarios
-         * Type: int 1 is enabled, 0 is disabled
-         *
-         * @hide
-         */
-        public static final String ENHANCED_CONNECTIVITY_ENABLED =
-                "enhanced_connectivity_enable";
-
-        /**
          * Overlay display devices setting.
          * The associated value is a specially formatted string that describes the
          * size and density of simulated secondary display devices.
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 6beea876..eaa7eaf 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -595,6 +595,256 @@
     }
 
     /**
+     * A common arguments class used for various screenshot requests. This contains arguments that
+     * are shared between {@link DisplayCaptureArgs} and {@link LayerCaptureArgs}
+     * @hide
+     */
+    public abstract static class CaptureArgs {
+        private final int mPixelFormat;
+        private final Rect mSourceCrop = new Rect();
+        private final float mFrameScale;
+        private final boolean mCaptureSecureLayers;
+
+        private CaptureArgs(Builder<? extends Builder<?>> builder) {
+            mPixelFormat = builder.mPixelFormat;
+            mSourceCrop.set(builder.mSourceCrop);
+            mFrameScale = builder.mFrameScale;
+            mCaptureSecureLayers = builder.mCaptureSecureLayers;
+        }
+
+        /**
+         * The Builder class used to construct {@link CaptureArgs}
+         *
+         * @param <T> A builder that extends {@link Builder}
+         */
+        public abstract static class Builder<T extends Builder<T>> {
+            private int mPixelFormat = PixelFormat.RGBA_8888;
+            private final Rect mSourceCrop = new Rect();
+            private float mFrameScale = 1;
+            private boolean mCaptureSecureLayers;
+
+            /**
+             * The desired pixel format of the returned buffer.
+             */
+            public T setPixelFormat(int pixelFormat) {
+                mPixelFormat = pixelFormat;
+                return getThis();
+            }
+
+            /**
+             * The portion of the screen to capture into the buffer. Caller may pass  in
+             * 'new Rect()' if no cropping is desired.
+             */
+            public T setSourceCrop(Rect sourceCrop) {
+                mSourceCrop.set(sourceCrop);
+                return getThis();
+            }
+
+            /**
+             * The desired scale of the returned buffer. The raw screen will be scaled up/down.
+             */
+            public T setFrameScale(float frameScale) {
+                mFrameScale = frameScale;
+                return getThis();
+            }
+
+            /**
+             * Whether to allow the screenshot of secure layers. Warning: This should only be done
+             * if the content will be placed in a secure SurfaceControl.
+             *
+             * @see ScreenshotHardwareBuffer#containsSecureLayers()
+             */
+            public T setCaptureSecureLayers(boolean captureSecureLayers) {
+                mCaptureSecureLayers = captureSecureLayers;
+                return getThis();
+            }
+
+            /**
+             * Each sub class should return itself to allow the builder to chain properly
+             */
+            public abstract T getThis();
+        }
+    }
+
+    /**
+     * The arguments class used to make display capture requests.
+     *
+     * @see #nativeScreenshot(IBinder, Rect, int, int, boolean, int, boolean)
+     * @hide
+     */
+    public static class DisplayCaptureArgs extends CaptureArgs {
+        private final IBinder mDisplayToken;
+        private final int mWidth;
+        private final int mHeight;
+        private final boolean mUseIdentityTransform;
+        private final int mRotation;
+
+        private DisplayCaptureArgs(Builder builder) {
+            super(builder);
+            mDisplayToken = builder.mDisplayToken;
+            mWidth = builder.mWidth;
+            mHeight = builder.mHeight;
+            mUseIdentityTransform = builder.mUseIdentityTransform;
+            mRotation = builder.mRotation;
+        }
+
+        /**
+         * The Builder class used to construct {@link DisplayCaptureArgs}
+         */
+        public static class Builder extends CaptureArgs.Builder<Builder> {
+            private IBinder mDisplayToken;
+            private int mWidth;
+            private int mHeight;
+            private boolean mUseIdentityTransform;
+            private @Surface.Rotation int mRotation = Surface.ROTATION_0;
+
+            /**
+             * Construct a new {@link LayerCaptureArgs} with the set parameters. The builder
+             * remains valid.
+             */
+            public DisplayCaptureArgs build() {
+                if (mDisplayToken == null) {
+                    throw new IllegalStateException(
+                            "Can't take screenshot with null display token");
+                }
+                return new DisplayCaptureArgs(this);
+            }
+
+            public Builder(IBinder displayToken) {
+                setDisplayToken(displayToken);
+            }
+
+            /**
+             * The display to take the screenshot of.
+             */
+            public Builder setDisplayToken(IBinder displayToken) {
+                mDisplayToken = displayToken;
+                return this;
+            }
+
+            /**
+             * Set the desired size of the returned buffer. The raw screen  will be  scaled down to
+             * this size
+             *
+             * @param width  The desired width of the returned buffer. Caller may pass in 0 if no
+             *               scaling is desired.
+             * @param height The desired height of the returned buffer. Caller may pass in 0 if no
+             *               scaling is desired.
+             */
+            public Builder setSize(int width, int height) {
+                mWidth = width;
+                mHeight = height;
+                return this;
+            }
+
+            /**
+             * Replace whatever transformation (rotation, scaling, translation) the surface
+             * layers are currently using with the identity transformation while taking the
+             * screenshot.
+             */
+            public Builder setUseIdentityTransform(boolean useIdentityTransform) {
+                mUseIdentityTransform = useIdentityTransform;
+                return this;
+            }
+
+            /**
+             * Apply a custom clockwise rotation to the screenshot, i.e.
+             * Surface.ROTATION_0,90,180,270. SurfaceFlinger will always take screenshots in its
+             * native portrait orientation by default, so this is useful for returning screenshots
+             * that are independent of device orientation.
+             */
+            public Builder setRotation(@Surface.Rotation int rotation) {
+                mRotation = rotation;
+                return this;
+            }
+
+            @Override
+            public Builder getThis() {
+                return this;
+            }
+        }
+    }
+
+    /**
+     * The arguments class used to make layer capture requests.
+     *
+     * @see #nativeCaptureLayers(IBinder, long, Rect, float, long[], int)
+     * @hide
+     */
+    public static class LayerCaptureArgs extends CaptureArgs {
+        private final long mNativeLayer;
+        private final long[] mNativeExcludeLayers;
+        private final boolean mChildrenOnly;
+
+        private LayerCaptureArgs(Builder builder) {
+            super(builder);
+            mChildrenOnly = builder.mChildrenOnly;
+            mNativeLayer = builder.mLayer.mNativeObject;
+            mNativeExcludeLayers = new long[builder.mExcludeLayers.length];
+            for (int i = 0; i < builder.mExcludeLayers.length; i++) {
+                mNativeExcludeLayers[i] = builder.mExcludeLayers[i].mNativeObject;
+            }
+        }
+
+        /**
+         * The Builder class used to construct {@link LayerCaptureArgs}
+         */
+        public static class Builder extends CaptureArgs.Builder<Builder> {
+            private SurfaceControl mLayer;
+            private SurfaceControl[] mExcludeLayers;
+            private boolean mChildrenOnly = true;
+
+            /**
+             * Construct a new {@link LayerCaptureArgs} with the set parameters. The builder
+             * remains valid.
+             */
+            public LayerCaptureArgs build() {
+                if (mLayer == null) {
+                    throw new IllegalStateException(
+                            "Can't take screenshot with null layer");
+                }
+                return new LayerCaptureArgs(this);
+            }
+
+            public Builder(SurfaceControl layer) {
+                setLayer(layer);
+            }
+
+            /**
+             * The root layer to capture.
+             */
+            public Builder setLayer(SurfaceControl layer) {
+                mLayer = layer;
+                return this;
+            }
+
+
+            /**
+             * An array of layer handles to exclude.
+             */
+            public Builder setExcludeLayers(@Nullable SurfaceControl[] excludeLayers) {
+                mExcludeLayers = excludeLayers;
+                return this;
+            }
+
+            /**
+             * Whether to include the layer itself in the screenshot or just the children and their
+             * descendants.
+             */
+            public Builder setChildrenOnly(boolean childrenOnly) {
+                mChildrenOnly = childrenOnly;
+                return this;
+            }
+
+            @Override
+            public Builder getThis() {
+                return this;
+            }
+
+        }
+    }
+
+    /**
      * Builder class for {@link SurfaceControl} objects.
      *
      * By default the surface will be hidden, and have "unset" bounds, meaning it can
@@ -1969,37 +2219,6 @@
     }
 
     /**
-     * @see SurfaceControl#screenshot(IBinder, Surface, Rect, int, int, boolean, int)
-     * @hide
-     */
-    public static void screenshot(IBinder display, Surface consumer) {
-        screenshot(display, consumer, new Rect(), 0, 0, false, 0);
-    }
-
-    /**
-     * Copy the current screen contents into the provided {@link Surface}
-     *
-     * @param consumer The {@link Surface} to take the screenshot into.
-     * @see SurfaceControl#screenshotToBuffer(IBinder, Rect, int, int, boolean, int)
-     * @hide
-     */
-    public static void screenshot(IBinder display, Surface consumer, Rect sourceCrop, int width,
-            int height, boolean useIdentityTransform, int rotation) {
-        if (consumer == null) {
-            throw new IllegalArgumentException("consumer must not be null");
-        }
-
-        final ScreenshotHardwareBuffer buffer = screenshotToBuffer(display, sourceCrop, width,
-                height, useIdentityTransform, rotation);
-        try {
-            consumer.attachAndQueueBufferWithColorSpace(buffer.getHardwareBuffer(),
-                    buffer.getColorSpace());
-        } catch (RuntimeException e) {
-            Log.w(TAG, "Failed to take screenshot - " + e.getMessage());
-        }
-    }
-
-    /**
      * @see SurfaceControl#screenshot(Rect, int, int, boolean, int)}
      * @hide
      */
@@ -2014,8 +2233,7 @@
      * a software Bitmap using {@link Bitmap#copy(Bitmap.Config, boolean)}
      *
      * CAVEAT: Versions of screenshot that return a {@link Bitmap} can be extremely slow; avoid use
-     * unless absolutely necessary; prefer the versions that use a {@link Surface} such as
-     * {@link SurfaceControl#screenshot(IBinder, Surface)} or {@link HardwareBuffer} such as
+     * unless absolutely necessary; prefer the versions that use a {@link HardwareBuffer} such as
      * {@link SurfaceControl#screenshotToBuffer(IBinder, Rect, int, int, boolean, int)}.
      *
      * @see SurfaceControl#screenshotToBuffer(IBinder, Rect, int, int, boolean, int)}
diff --git a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java
index cce1090..6300320 100644
--- a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java
+++ b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java
@@ -47,6 +47,9 @@
     /**
      * Max number of suggestions expected from the response. It must be a positive value.
      * Defaults to {@code SUGGESTION_COUNT_UNLIMITED} if not set.
+     *
+     * <p>In practice, it is recommended that the max suggestion count does not exceed <b>5</b>
+     * for performance reasons.</p>
      */
     private final int mMaxSuggestionCount;
 
@@ -67,6 +70,9 @@
     /**
      * The IME provided locales for the request. If non-empty, the inline suggestions should
      * return languages from the supported locales. If not provided, it'll default to system locale.
+     *
+     * <p>Note for Autofill Providers: It is <b>recommended</b> for the returned inline suggestions
+     * to have one locale to guarantee consistent UI rendering.</p>
      */
     private @NonNull LocaleList mSupportedLocales;
 
@@ -227,6 +233,9 @@
     /**
      * Max number of suggestions expected from the response. It must be a positive value.
      * Defaults to {@code SUGGESTION_COUNT_UNLIMITED} if not set.
+     *
+     * <p>In practice, it is recommended that the max suggestion count does not exceed <b>5</b>
+     * for performance reasons.</p>
      */
     @DataClass.Generated.Member
     public int getMaxSuggestionCount() {
@@ -256,6 +265,9 @@
     /**
      * The IME provided locales for the request. If non-empty, the inline suggestions should
      * return languages from the supported locales. If not provided, it'll default to system locale.
+     *
+     * <p>Note for Autofill Providers: It is <b>recommended</b> for the returned inline suggestions
+     * to have one locale to guarantee consistent UI rendering.</p>
      */
     @DataClass.Generated.Member
     public @NonNull LocaleList getSupportedLocales() {
@@ -458,6 +470,9 @@
         /**
          * Max number of suggestions expected from the response. It must be a positive value.
          * Defaults to {@code SUGGESTION_COUNT_UNLIMITED} if not set.
+         *
+         * <p>In practice, it is recommended that the max suggestion count does not exceed <b>5</b>
+         * for performance reasons.</p>
          */
         @DataClass.Generated.Member
         public @NonNull Builder setMaxSuggestionCount(int value) {
@@ -508,6 +523,9 @@
         /**
          * The IME provided locales for the request. If non-empty, the inline suggestions should
          * return languages from the supported locales. If not provided, it'll default to system locale.
+         *
+         * <p>Note for Autofill Providers: It is <b>recommended</b> for the returned inline suggestions
+         * to have one locale to guarantee consistent UI rendering.</p>
          */
         @DataClass.Generated.Member
         public @NonNull Builder setSupportedLocales(@NonNull LocaleList value) {
@@ -604,7 +622,7 @@
     }
 
     @DataClass.Generated(
-            time = 1588109685838L,
+            time = 1595457701315L,
             codegenVersion = "1.0.15",
             sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestionsRequest.java",
             inputSignatures = "public static final  int SUGGESTION_COUNT_UNLIMITED\nprivate final  int mMaxSuggestionCount\nprivate final @android.annotation.NonNull java.util.List<android.widget.inline.InlinePresentationSpec> mInlinePresentationSpecs\nprivate @android.annotation.NonNull java.lang.String mHostPackageName\nprivate @android.annotation.NonNull android.os.LocaleList mSupportedLocales\nprivate @android.annotation.NonNull android.os.Bundle mExtras\nprivate @android.annotation.Nullable android.os.IBinder mHostInputToken\nprivate  int mHostDisplayId\npublic  void setHostInputToken(android.os.IBinder)\nprivate  boolean extrasEquals(android.os.Bundle)\nprivate  void parcelHostInputToken(android.os.Parcel,int)\nprivate @android.annotation.Nullable android.os.IBinder unparcelHostInputToken(android.os.Parcel)\npublic  void setHostDisplayId(int)\nprivate  void onConstructed()\npublic  void filterContentTypes()\nprivate static  int defaultMaxSuggestionCount()\nprivate static  java.lang.String defaultHostPackageName()\nprivate static  android.os.LocaleList defaultSupportedLocales()\nprivate static @android.annotation.Nullable android.os.IBinder defaultHostInputToken()\nprivate static @android.annotation.Nullable int defaultHostDisplayId()\nprivate static @android.annotation.NonNull android.os.Bundle defaultExtras()\nclass InlineSuggestionsRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nabstract  android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(java.util.List<android.widget.inline.InlinePresentationSpec>)\nabstract  android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract  android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nabstract  android.view.inputmethod.InlineSuggestionsRequest.Builder setHostDisplayId(int)\nclass BaseBuilder extends java.lang.Object implements []")
diff --git a/core/java/android/view/inputmethod/InlineSuggestionsResponse.java b/core/java/android/view/inputmethod/InlineSuggestionsResponse.java
index be833df..b393c67 100644
--- a/core/java/android/view/inputmethod/InlineSuggestionsResponse.java
+++ b/core/java/android/view/inputmethod/InlineSuggestionsResponse.java
@@ -32,7 +32,18 @@
  */
 @DataClass(genEqualsHashCode = true, genToString = true, genHiddenConstructor = true)
 public final class InlineSuggestionsResponse implements Parcelable {
-    private final @NonNull List<InlineSuggestion> mInlineSuggestions;
+    /**
+     * List of {@link InlineSuggestion}s returned as a part of this response.
+     *
+     * <p>When the host app requests to inflate this <b>ordered</b> list of inline suggestions by
+     * calling {@link InlineSuggestion#inflate}, it is the host's responsibility to track the
+     * order of the inflated {@link android.view.View}s. These views are to be added in
+     * order to the view hierarchy, because the inflation calls will return asynchronously.</p>
+     *
+     * <p>The inflation ordering does not apply to the pinned icon.</p>
+     */
+    @NonNull
+    private final List<InlineSuggestion> mInlineSuggestions;
 
     /**
      * Creates a new {@link InlineSuggestionsResponse}, for testing purpose.
@@ -48,7 +59,7 @@
 
 
 
-    // Code below generated by codegen v1.0.14.
+    // Code below generated by codegen v1.0.15.
     //
     // DO NOT MODIFY!
     // CHECKSTYLE:OFF Generated code
@@ -64,6 +75,15 @@
     /**
      * Creates a new InlineSuggestionsResponse.
      *
+     * @param inlineSuggestions
+     *   List of {@link InlineSuggestion}s returned as a part of this response.
+     *
+     *   <p>When the host app requests to inflate this <b>ordered</b> list of inline suggestions by
+     *   calling {@link InlineSuggestion#inflate}, it is the host's responsibility to track the
+     *   order of the inflated {@link android.view.View}s. These views are to be added in
+     *   order to the view hierarchy, because the inflation calls will return asynchronously.</p>
+     *
+     *   <p>The inflation ordering does not apply to the pinned icon.</p>
      * @hide
      */
     @DataClass.Generated.Member
@@ -76,6 +96,16 @@
         // onConstructed(); // You can define this method to get a callback
     }
 
+    /**
+     * List of {@link InlineSuggestion}s returned as a part of this response.
+     *
+     * <p>When the host app requests to inflate this <b>ordered</b> list of inline suggestions by
+     * calling {@link InlineSuggestion#inflate}, it is the host's responsibility to track the
+     * order of the inflated {@link android.view.View}s. These views are to be added in
+     * order to the view hierarchy, because the inflation calls will return asynchronously.</p>
+     *
+     * <p>The inflation ordering does not apply to the pinned icon.</p>
+     */
     @DataClass.Generated.Member
     public @NonNull List<InlineSuggestion> getInlineSuggestions() {
         return mInlineSuggestions;
@@ -164,8 +194,8 @@
     };
 
     @DataClass.Generated(
-            time = 1578972149519L,
-            codegenVersion = "1.0.14",
+            time = 1595891876037L,
+            codegenVersion = "1.0.15",
             sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestionsResponse.java",
             inputSignatures = "private final @android.annotation.NonNull java.util.List<android.view.inputmethod.InlineSuggestion> mInlineSuggestions\npublic static @android.annotation.TestApi @android.annotation.NonNull android.view.inputmethod.InlineSuggestionsResponse newInlineSuggestionsResponse(java.util.List<android.view.inputmethod.InlineSuggestion>)\nclass InlineSuggestionsResponse extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstructor=true)")
     @Deprecated
diff --git a/core/tests/bugreports/Android.bp b/core/tests/bugreports/Android.bp
index 1edd962..e42b4b4 100644
--- a/core/tests/bugreports/Android.bp
+++ b/core/tests/bugreports/Android.bp
@@ -20,7 +20,11 @@
         "android.test.runner",
         "android.test.base",
     ],
-    static_libs: ["androidx.test.rules", "truth-prebuilt"],
+    static_libs: [
+        "androidx.test.rules",
+        "androidx.test.uiautomator_uiautomator",
+        "truth-prebuilt",
+    ],
     test_suites: ["general-tests"],
     sdk_version: "test_current",
     platform_apis: true,
diff --git a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
index 1533377..9246a23 100644
--- a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
+++ b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
@@ -18,6 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import android.Manifest;
@@ -25,17 +27,27 @@
 import android.os.BugreportManager;
 import android.os.BugreportManager.BugreportCallback;
 import android.os.BugreportParams;
+import android.os.FileUtils;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.ParcelFileDescriptor;
+import android.os.StrictMode;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExternalResource;
 import org.junit.rules.TestName;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -51,9 +63,11 @@
 @RunWith(JUnit4.class)
 public class BugreportManagerTest {
     @Rule public TestName name = new TestName();
+    @Rule public ExtendedStrictModeVmPolicy mTemporaryVmPolicy = new ExtendedStrictModeVmPolicy();
 
     private static final String TAG = "BugreportManagerTest";
     private static final long BUGREPORT_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(10);
+    private static final long UIAUTOMATOR_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
 
     private Handler mHandler;
     private Executor mExecutor;
@@ -86,6 +100,8 @@
     @After
     public void teardown() throws Exception {
         dropPermissions();
+        FileUtils.closeQuietly(mBugreportFd);
+        FileUtils.closeQuietly(mScreenshotFd);
     }
 
 
@@ -95,47 +111,45 @@
         // wifi bugreport does not take screenshot
         mBrm.startBugreport(mBugreportFd, null /*screenshotFd = null*/, wifi(),
                 mExecutor, callback);
+        shareConsentDialog(ConsentReply.ALLOW);
         waitTillDoneOrTimeout(callback);
 
         assertThat(callback.isDone()).isTrue();
         // Wifi bugreports should not receive any progress.
         assertThat(callback.hasReceivedProgress()).isFalse();
-        // TODO: Because of b/130234145, consent dialog is not shown; so we get a timeout error.
-        // When the bug is fixed, accept consent via UIAutomator and verify contents
-        // of mBugreportFd.
-        assertThat(callback.getErrorCode()).isEqualTo(
-                BugreportCallback.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT);
+        assertThat(mBugreportFile.length()).isGreaterThan(0L);
         assertFdsAreClosed(mBugreportFd);
     }
 
+    @LargeTest
     @Test
     public void normalFlow_interactive() throws Exception {
         BugreportCallbackImpl callback = new BugreportCallbackImpl();
         // interactive bugreport does not take screenshot
         mBrm.startBugreport(mBugreportFd, null /*screenshotFd = null*/, interactive(),
                 mExecutor, callback);
-
+        shareConsentDialog(ConsentReply.ALLOW);
         waitTillDoneOrTimeout(callback);
+
         assertThat(callback.isDone()).isTrue();
         // Interactive bugreports show progress updates.
         assertThat(callback.hasReceivedProgress()).isTrue();
-        assertThat(callback.getErrorCode()).isEqualTo(
-                BugreportCallback.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT);
+        assertThat(mBugreportFile.length()).isGreaterThan(0L);
         assertFdsAreClosed(mBugreportFd);
     }
 
+    @LargeTest
     @Test
     public void normalFlow_full() throws Exception {
         BugreportCallbackImpl callback = new BugreportCallbackImpl();
         mBrm.startBugreport(mBugreportFd, mScreenshotFd, full(), mExecutor, callback);
-
+        shareConsentDialog(ConsentReply.ALLOW);
         waitTillDoneOrTimeout(callback);
+
         assertThat(callback.isDone()).isTrue();
-        assertThat(callback.getErrorCode()).isEqualTo(
-                BugreportCallback.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT);
-        // bugreport and screenshot files should be empty when user consent timed out.
-        assertThat(mBugreportFile.length()).isEqualTo(0);
-        assertThat(mScreenshotFile.length()).isEqualTo(0);
+        // bugreport and screenshot files shouldn't be empty when user consents.
+        assertThat(mBugreportFile.length()).isGreaterThan(0L);
+        assertThat(mScreenshotFile.length()).isGreaterThan(0L);
         assertFdsAreClosed(mBugreportFd, mScreenshotFd);
     }
 
@@ -144,6 +158,8 @@
         // Start bugreport #1
         BugreportCallbackImpl callback = new BugreportCallbackImpl();
         mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback);
+        // TODO(b/162389762) Make sure the wait time is reasonable
+        shareConsentDialog(ConsentReply.ALLOW);
 
         // Before #1 is done, try to start #2.
         assertThat(callback.isDone()).isFalse();
@@ -375,4 +391,88 @@
     private static BugreportParams full() {
         return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL);
     }
+
+    /* Allow/deny the consent dialog to sharing bugreport data or check existence only. */
+    private enum ConsentReply {
+        ALLOW,
+        DENY,
+        TIMEOUT
+    }
+
+    /*
+     * Ensure the consent dialog is shown and take action according to <code>consentReply<code/>.
+     * It will fail if the dialog is not shown when <code>ignoreNotFound<code/> is false.
+     */
+    private void shareConsentDialog(@NonNull ConsentReply consentReply) throws Exception {
+        mTemporaryVmPolicy.permitIncorrectContextUse();
+        final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+
+        // Unlock before finding/clicking an object.
+        device.wakeUp();
+        device.executeShellCommand("wm dismiss-keyguard");
+
+        final BySelector consentTitleObj = By.res("android", "alertTitle");
+        if (!device.wait(Until.hasObject(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS)) {
+            fail("The consent dialog is not found");
+        }
+        if (consentReply.equals(ConsentReply.TIMEOUT)) {
+            return;
+        }
+        final BySelector selector;
+        if (consentReply.equals(ConsentReply.ALLOW)) {
+            selector = By.res("android", "button1");
+            Log.d(TAG, "Allow the consent dialog");
+        } else { // ConsentReply.DENY
+            selector = By.res("android", "button2");
+            Log.d(TAG, "Deny the consent dialog");
+        }
+        final UiObject2 btnObj = device.findObject(selector);
+        assertNotNull("The button of consent dialog is not found", btnObj);
+        btnObj.click();
+
+        Log.d(TAG, "Wait for the dialog to be dismissed");
+        assertTrue(device.wait(Until.gone(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS));
+    }
+
+    /**
+     * A rule to change strict mode vm policy temporarily till test method finished.
+     *
+     * To permit the non-visual context usage in tests while taking bugreports need user consent,
+     * or UiAutomator/BugreportManager.DumpstateListener would run into error.
+     * UiDevice#findObject creates UiObject2, its Gesture object and ViewConfiguration and
+     * UiObject2#click need to know bounds. Both of them access to WindowManager internally without
+     * visual context comes from InstrumentationRegistry and violate the policy.
+     * Also <code>DumpstateListener<code/> violate the policy when onScreenshotTaken is called.
+     *
+     * TODO(b/161201609) Remove this class once violations fixed.
+     */
+    static class ExtendedStrictModeVmPolicy extends ExternalResource {
+        private boolean mWasVmPolicyChanged = false;
+        private StrictMode.VmPolicy mOldVmPolicy;
+
+        @Override
+        protected void after() {
+            restoreVmPolicyIfNeeded();
+        }
+
+        public void permitIncorrectContextUse() {
+            // Allow to call multiple times without losing old policy.
+            if (mOldVmPolicy == null) {
+                mOldVmPolicy = StrictMode.getVmPolicy();
+            }
+            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
+                    .detectAll()
+                    .permitIncorrectContextUse()
+                    .penaltyLog()
+                    .build());
+            mWasVmPolicyChanged = true;
+        }
+
+        private void restoreVmPolicyIfNeeded() {
+            if (mWasVmPolicyChanged && mOldVmPolicy != null) {
+                StrictMode.setVmPolicy(mOldVmPolicy);
+                mOldVmPolicy = null;
+            }
+        }
+    }
 }
diff --git a/packages/CarSystemUI/res/values/config.xml b/packages/CarSystemUI/res/values/config.xml
index cf967c0..039f2c0 100644
--- a/packages/CarSystemUI/res/values/config.xml
+++ b/packages/CarSystemUI/res/values/config.xml
@@ -24,12 +24,35 @@
 
     <bool name="config_enableFullscreenUserSwitcher">true</bool>
 
-    <!-- configure which system ui bars should be displayed -->
+    <!-- Configure which system bars should be displayed. -->
     <bool name="config_enableTopNavigationBar">true</bool>
     <bool name="config_enableLeftNavigationBar">false</bool>
     <bool name="config_enableRightNavigationBar">false</bool>
     <bool name="config_enableBottomNavigationBar">true</bool>
 
+    <!-- Configure the type of each system bar. Each system bar must have a unique type. -->
+    <!--    STATUS_BAR = 0-->
+    <!--    NAVIGATION_BAR = 1-->
+    <!--    STATUS_BAR_EXTRA = 2-->
+    <!--    NAVIGATION_BAR_EXTRA = 3-->
+    <integer name="config_topSystemBarType">0</integer>
+    <integer name="config_leftSystemBarType">2</integer>
+    <integer name="config_rightSystemBarType">3</integer>
+    <integer name="config_bottomSystemBarType">1</integer>
+
+    <!-- Configure the relative z-order among the system bars. When two system bars overlap (e.g.
+         if both top bar and left bar are enabled, it creates an overlapping space in the upper left
+         corner), the system bar with the higher z-order takes the overlapping space and padding is
+         applied to the other bar.-->
+    <!-- NOTE: If two overlapping system bars have the same z-order, SystemBarConfigs will throw a
+         RuntimeException, since their placing order cannot be determined. Bars that do not overlap
+         are allowed to have the same z-order. -->
+    <!-- NOTE: If the z-order of a bar is 10 or above, it will also appear on top of HUN's.    -->
+    <integer name="config_topSystemBarZOrder">1</integer>
+    <integer name="config_leftSystemBarZOrder">0</integer>
+    <integer name="config_rightSystemBarZOrder">0</integer>
+    <integer name="config_bottomSystemBarZOrder">10</integer>
+
     <!-- Disable normal notification rendering; we handle that ourselves -->
     <bool name="config_renderNotifications">false</bool>
 
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBar.java b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBar.java
index 35b2080..9584850 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBar.java
@@ -16,12 +16,8 @@
 
 package com.android.systemui.car.navigationbar;
 
-import static android.view.InsetsState.ITYPE_BOTTOM_GESTURES;
-import static android.view.InsetsState.ITYPE_CLIMATE_BAR;
-import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
-import static android.view.InsetsState.ITYPE_TOP_GESTURES;
 import static android.view.InsetsState.containsType;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 
@@ -30,13 +26,11 @@
 
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.PixelFormat;
 import android.inputmethodservice.InputMethodService;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.view.Display;
-import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsetsController;
@@ -47,7 +41,6 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.RegisterStatusBarResult;
 import com.android.internal.view.AppearanceRegion;
-import com.android.systemui.R;
 import com.android.systemui.SystemUI;
 import com.android.systemui.car.CarDeviceProvisionedController;
 import com.android.systemui.car.CarDeviceProvisionedListener;
@@ -76,7 +69,6 @@
 
 /** Navigation bars customized for the automotive use case. */
 public class CarNavigationBar extends SystemUI implements CommandQueue.Callbacks {
-
     private final Resources mResources;
     private final CarNavigationBarController mCarNavigationBarController;
     private final SysuiDarkIconDispatcher mStatusBarIconController;
@@ -93,6 +85,7 @@
     private final Lazy<StatusBarIconController> mIconControllerLazy;
 
     private final int mDisplayId;
+    private final SystemBarConfigs mSystemBarConfigs;
 
     private StatusBarSignalPolicy mSignalPolicy;
     private ActivityManagerWrapper mActivityManagerWrapper;
@@ -141,7 +134,8 @@
             IStatusBarService barService,
             Lazy<KeyguardStateController> keyguardStateControllerLazy,
             Lazy<PhoneStatusBarPolicy> iconPolicyLazy,
-            Lazy<StatusBarIconController> iconControllerLazy
+            Lazy<StatusBarIconController> iconControllerLazy,
+            SystemBarConfigs systemBarConfigs
     ) {
         super(context);
         mResources = resources;
@@ -158,6 +152,7 @@
         mKeyguardStateControllerLazy = keyguardStateControllerLazy;
         mIconPolicyLazy = iconPolicyLazy;
         mIconControllerLazy = iconControllerLazy;
+        mSystemBarConfigs = systemBarConfigs;
 
         mDisplayId = context.getDisplayId();
     }
@@ -344,103 +339,63 @@
     private void buildNavBarContent() {
         mTopNavigationBarView = mCarNavigationBarController.getTopBar(isDeviceSetupForUser());
         if (mTopNavigationBarView != null) {
+            mSystemBarConfigs.insetSystemBar(SystemBarConfigs.TOP, mTopNavigationBarView);
             mTopNavigationBarWindow.addView(mTopNavigationBarView);
         }
 
         mBottomNavigationBarView = mCarNavigationBarController.getBottomBar(isDeviceSetupForUser());
         if (mBottomNavigationBarView != null) {
+            mSystemBarConfigs.insetSystemBar(SystemBarConfigs.BOTTOM, mBottomNavigationBarView);
             mBottomNavigationBarWindow.addView(mBottomNavigationBarView);
         }
 
         mLeftNavigationBarView = mCarNavigationBarController.getLeftBar(isDeviceSetupForUser());
         if (mLeftNavigationBarView != null) {
+            mSystemBarConfigs.insetSystemBar(SystemBarConfigs.LEFT, mLeftNavigationBarView);
             mLeftNavigationBarWindow.addView(mLeftNavigationBarView);
         }
 
         mRightNavigationBarView = mCarNavigationBarController.getRightBar(isDeviceSetupForUser());
         if (mRightNavigationBarView != null) {
+            mSystemBarConfigs.insetSystemBar(SystemBarConfigs.RIGHT, mRightNavigationBarView);
             mRightNavigationBarWindow.addView(mRightNavigationBarView);
         }
     }
 
     private void attachNavBarWindows() {
-        if (mTopNavigationBarWindow != null) {
-            int height = mResources.getDimensionPixelSize(
-                    com.android.internal.R.dimen.status_bar_height);
-            WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
-                    ViewGroup.LayoutParams.MATCH_PARENT,
-                    height,
-                    WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL,
-                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
-                            | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
-                            | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
-                    PixelFormat.TRANSLUCENT);
-            lp.setTitle("TopCarNavigationBar");
-            lp.providesInsetsTypes = new int[]{ITYPE_STATUS_BAR, ITYPE_TOP_GESTURES};
-            lp.setFitInsetsTypes(0);
-            lp.windowAnimations = 0;
-            lp.gravity = Gravity.TOP;
-            mWindowManager.addView(mTopNavigationBarWindow, lp);
-        }
+        mSystemBarConfigs.getSystemBarSidesByZOrder().forEach(this::attachNavBarBySide);
+    }
 
-        if (mBottomNavigationBarWindow != null && !mBottomNavBarVisible) {
-            mBottomNavBarVisible = true;
-            int height = mResources.getDimensionPixelSize(
-                    com.android.internal.R.dimen.navigation_bar_height);
+    private void attachNavBarBySide(int side) {
+        switch(side) {
+            case SystemBarConfigs.TOP:
+                if (mTopNavigationBarWindow != null) {
+                    mWindowManager.addView(mTopNavigationBarWindow,
+                            mSystemBarConfigs.getLayoutParamsBySide(SystemBarConfigs.TOP));
+                }
+                break;
+            case SystemBarConfigs.BOTTOM:
+                if (mBottomNavigationBarWindow != null && !mBottomNavBarVisible) {
+                    mBottomNavBarVisible = true;
 
-            WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
-                    ViewGroup.LayoutParams.MATCH_PARENT,
-                    height,
-                    WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
-                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
-                            | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
-                            | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
-                    PixelFormat.TRANSLUCENT);
-            lp.setTitle("BottomCarNavigationBar");
-            lp.providesInsetsTypes = new int[]{ITYPE_NAVIGATION_BAR, ITYPE_BOTTOM_GESTURES};
-            lp.windowAnimations = 0;
-            lp.gravity = Gravity.BOTTOM;
-            mWindowManager.addView(mBottomNavigationBarWindow, lp);
-        }
-
-        if (mLeftNavigationBarWindow != null) {
-            int width = mResources.getDimensionPixelSize(
-                    R.dimen.car_left_navigation_bar_width);
-            WindowManager.LayoutParams leftlp = new WindowManager.LayoutParams(
-                    width, ViewGroup.LayoutParams.MATCH_PARENT,
-                    WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
-                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
-                            | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
-                            | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
-                    PixelFormat.TRANSLUCENT);
-            leftlp.setTitle("LeftCarNavigationBar");
-            leftlp.providesInsetsTypes = new int[]{ITYPE_CLIMATE_BAR};
-            leftlp.setFitInsetsTypes(0);
-            leftlp.windowAnimations = 0;
-            leftlp.gravity = Gravity.LEFT;
-            mWindowManager.addView(mLeftNavigationBarWindow, leftlp);
-        }
-
-        if (mRightNavigationBarWindow != null) {
-            int width = mResources.getDimensionPixelSize(
-                    R.dimen.car_right_navigation_bar_width);
-            WindowManager.LayoutParams rightlp = new WindowManager.LayoutParams(
-                    width, ViewGroup.LayoutParams.MATCH_PARENT,
-                    WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
-                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
-                            | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
-                            | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
-                    PixelFormat.TRANSLUCENT);
-            rightlp.setTitle("RightCarNavigationBar");
-            rightlp.providesInsetsTypes = new int[]{ITYPE_EXTRA_NAVIGATION_BAR};
-            rightlp.setFitInsetsTypes(0);
-            rightlp.windowAnimations = 0;
-            rightlp.gravity = Gravity.RIGHT;
-            mWindowManager.addView(mRightNavigationBarWindow, rightlp);
+                    mWindowManager.addView(mBottomNavigationBarWindow,
+                            mSystemBarConfigs.getLayoutParamsBySide(SystemBarConfigs.BOTTOM));
+                }
+                break;
+            case SystemBarConfigs.LEFT:
+                if (mLeftNavigationBarWindow != null) {
+                    mWindowManager.addView(mLeftNavigationBarWindow,
+                            mSystemBarConfigs.getLayoutParamsBySide(SystemBarConfigs.LEFT));
+                }
+                break;
+            case SystemBarConfigs.RIGHT:
+                if (mRightNavigationBarWindow != null) {
+                    mWindowManager.addView(mRightNavigationBarWindow,
+                            mSystemBarConfigs.getLayoutParamsBySide(SystemBarConfigs.RIGHT));
+                }
+                break;
+            default:
+                return;
         }
     }
 
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarController.java b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarController.java
index ca780ae..fe26040 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarController.java
@@ -22,7 +22,6 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.systemui.R;
 import com.android.systemui.car.hvac.HvacController;
 
 import javax.inject.Inject;
@@ -61,7 +60,8 @@
             NavigationBarViewFactory navigationBarViewFactory,
             ButtonSelectionStateController buttonSelectionStateController,
             Lazy<HvacController> hvacControllerLazy,
-            ButtonRoleHolderController buttonRoleHolderController) {
+            ButtonRoleHolderController buttonRoleHolderController,
+            SystemBarConfigs systemBarConfigs) {
         mContext = context;
         mNavigationBarViewFactory = navigationBarViewFactory;
         mButtonSelectionStateController = buttonSelectionStateController;
@@ -69,10 +69,10 @@
         mButtonRoleHolderController = buttonRoleHolderController;
 
         // Read configuration.
-        mShowTop = mContext.getResources().getBoolean(R.bool.config_enableTopNavigationBar);
-        mShowBottom = mContext.getResources().getBoolean(R.bool.config_enableBottomNavigationBar);
-        mShowLeft = mContext.getResources().getBoolean(R.bool.config_enableLeftNavigationBar);
-        mShowRight = mContext.getResources().getBoolean(R.bool.config_enableRightNavigationBar);
+        mShowTop = systemBarConfigs.getEnabledStatusBySide(SystemBarConfigs.TOP);
+        mShowBottom = systemBarConfigs.getEnabledStatusBySide(SystemBarConfigs.BOTTOM);
+        mShowLeft = systemBarConfigs.getEnabledStatusBySide(SystemBarConfigs.LEFT);
+        mShowRight = systemBarConfigs.getEnabledStatusBySide(SystemBarConfigs.RIGHT);
     }
 
     /**
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarView.java b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarView.java
index 0ced402..ab401bb 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarView.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarView.java
@@ -16,14 +16,10 @@
 
 package com.android.systemui.car.navigationbar;
 
-import static android.view.WindowInsets.Type.systemBars;
-
 import android.content.Context;
-import android.graphics.Insets;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.WindowInsets;
 import android.widget.LinearLayout;
 
 import com.android.systemui.Dependency;
@@ -80,30 +76,6 @@
         setFocusable(false);
     }
 
-    @Override
-    public WindowInsets onApplyWindowInsets(WindowInsets windowInsets) {
-        applyMargins(windowInsets.getInsets(systemBars()));
-        return windowInsets;
-    }
-
-    private void applyMargins(Insets insets) {
-        final int count = getChildCount();
-        for (int i = 0; i < count; i++) {
-            View child = getChildAt(i);
-            if (child.getLayoutParams() instanceof LayoutParams) {
-                LayoutParams lp = (LayoutParams) child.getLayoutParams();
-                if (lp.rightMargin != insets.right || lp.leftMargin != insets.left
-                        || lp.topMargin != insets.top || lp.bottomMargin != insets.bottom) {
-                    lp.rightMargin = insets.right;
-                    lp.leftMargin = insets.left;
-                    lp.topMargin = insets.top;
-                    lp.bottomMargin = insets.bottom;
-                    child.requestLayout();
-                }
-            }
-        }
-    }
-
     // Used to forward touch events even if the touch was initiated from a child component
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/SystemBarConfigs.java b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/SystemBarConfigs.java
new file mode 100644
index 0000000..3527bf9
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/SystemBarConfigs.java
@@ -0,0 +1,380 @@
+/*
+ * 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.car.navigationbar;
+
+import android.annotation.IntDef;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.InsetsState;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Main;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Reads configs for system bars for each side (TOP, BOTTOM, LEFT, and RIGHT) and returns the
+ * corresponding {@link android.view.WindowManager.LayoutParams} per the configuration.
+ */
+@Singleton
+public class SystemBarConfigs {
+
+    private static final String TAG = SystemBarConfigs.class.getSimpleName();
+    // The z-order from which system bars will start to appear on top of HUN's.
+    private static final int HUN_ZORDER = 10;
+
+    @IntDef(value = {TOP, BOTTOM, LEFT, RIGHT})
+    @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+    private @interface SystemBarSide {
+    }
+
+    public static final int TOP = 0;
+    public static final int BOTTOM = 1;
+    public static final int LEFT = 2;
+    public static final int RIGHT = 3;
+
+    /*
+        NOTE: The elements' order in the map below must be preserved as-is since the correct
+        corresponding values are obtained by the index.
+     */
+    private static final int[] BAR_TYPE_MAP = {
+            InsetsState.ITYPE_STATUS_BAR,
+            InsetsState.ITYPE_NAVIGATION_BAR,
+            InsetsState.ITYPE_CLIMATE_BAR,
+            InsetsState.ITYPE_EXTRA_NAVIGATION_BAR
+    };
+
+    private static final Map<@SystemBarSide Integer, Integer> BAR_GRAVITY_MAP = new ArrayMap<>();
+    private static final Map<@SystemBarSide Integer, String> BAR_TITLE_MAP = new ArrayMap<>();
+    private static final Map<@SystemBarSide Integer, Integer> BAR_GESTURE_MAP = new ArrayMap<>();
+
+    private final Resources mResources;
+    private final Map<@SystemBarSide Integer, SystemBarConfig> mSystemBarConfigMap =
+            new ArrayMap<>();
+    private final List<@SystemBarSide Integer> mSystemBarSidesByZOrder = new ArrayList<>();
+
+    private boolean mTopNavBarEnabled;
+    private boolean mBottomNavBarEnabled;
+    private boolean mLeftNavBarEnabled;
+    private boolean mRightNavBarEnabled;
+
+    @Inject
+    public SystemBarConfigs(@Main Resources resources) {
+        mResources = resources;
+
+        populateMaps();
+        readConfigs();
+        checkEnabledBarsHaveUniqueBarTypes();
+        setInsetPaddingsForOverlappingCorners();
+        sortSystemBarSidesByZOrder();
+    }
+
+    protected WindowManager.LayoutParams getLayoutParamsBySide(@SystemBarSide int side) {
+        return mSystemBarConfigMap.get(side) != null
+                ? mSystemBarConfigMap.get(side).getLayoutParams() : null;
+    }
+
+    protected boolean getEnabledStatusBySide(@SystemBarSide int side) {
+        switch (side) {
+            case TOP:
+                return mTopNavBarEnabled;
+            case BOTTOM:
+                return mBottomNavBarEnabled;
+            case LEFT:
+                return mLeftNavBarEnabled;
+            case RIGHT:
+                return mRightNavBarEnabled;
+            default:
+                return false;
+        }
+    }
+
+    protected void insetSystemBar(@SystemBarSide int side, CarNavigationBarView view) {
+        int[] paddings = mSystemBarConfigMap.get(side).getPaddings();
+        view.setPadding(paddings[2], paddings[0], paddings[3], paddings[1]);
+    }
+
+    protected List<Integer> getSystemBarSidesByZOrder() {
+        return mSystemBarSidesByZOrder;
+    }
+
+    @VisibleForTesting
+    protected static int getHunZOrder() {
+        return HUN_ZORDER;
+    }
+
+    private static void populateMaps() {
+        BAR_GRAVITY_MAP.put(TOP, Gravity.TOP);
+        BAR_GRAVITY_MAP.put(BOTTOM, Gravity.BOTTOM);
+        BAR_GRAVITY_MAP.put(LEFT, Gravity.LEFT);
+        BAR_GRAVITY_MAP.put(RIGHT, Gravity.RIGHT);
+
+        BAR_TITLE_MAP.put(TOP, "TopCarSystemBar");
+        BAR_TITLE_MAP.put(BOTTOM, "BottomCarSystemBar");
+        BAR_TITLE_MAP.put(LEFT, "LeftCarSystemBar");
+        BAR_TITLE_MAP.put(RIGHT, "RightCarSystemBar");
+
+        BAR_GESTURE_MAP.put(TOP, InsetsState.ITYPE_TOP_GESTURES);
+        BAR_GESTURE_MAP.put(BOTTOM, InsetsState.ITYPE_BOTTOM_GESTURES);
+        BAR_GESTURE_MAP.put(LEFT, InsetsState.ITYPE_LEFT_GESTURES);
+        BAR_GESTURE_MAP.put(RIGHT, InsetsState.ITYPE_RIGHT_GESTURES);
+    }
+
+    private void readConfigs() {
+        mTopNavBarEnabled = mResources.getBoolean(R.bool.config_enableTopNavigationBar);
+        mBottomNavBarEnabled = mResources.getBoolean(R.bool.config_enableBottomNavigationBar);
+        mLeftNavBarEnabled = mResources.getBoolean(R.bool.config_enableLeftNavigationBar);
+        mRightNavBarEnabled = mResources.getBoolean(R.bool.config_enableRightNavigationBar);
+
+        if (mTopNavBarEnabled) {
+            SystemBarConfig topBarConfig =
+                    new SystemBarConfigBuilder()
+                            .setSide(TOP)
+                            .setGirth(mResources.getDimensionPixelSize(
+                                    com.android.internal.R.dimen.status_bar_height))
+                            .setBarType(mResources.getInteger(R.integer.config_topSystemBarType))
+                            .setZOrder(mResources.getInteger(R.integer.config_topSystemBarZOrder))
+                            .build();
+            mSystemBarConfigMap.put(TOP, topBarConfig);
+        }
+
+        if (mBottomNavBarEnabled) {
+            SystemBarConfig bottomBarConfig =
+                    new SystemBarConfigBuilder()
+                            .setSide(BOTTOM)
+                            .setGirth(mResources.getDimensionPixelSize(
+                                    com.android.internal.R.dimen.navigation_bar_height))
+                            .setBarType(mResources.getInteger(R.integer.config_bottomSystemBarType))
+                            .setZOrder(
+                                    mResources.getInteger(R.integer.config_bottomSystemBarZOrder))
+                            .build();
+            mSystemBarConfigMap.put(BOTTOM, bottomBarConfig);
+        }
+
+        if (mLeftNavBarEnabled) {
+            SystemBarConfig leftBarConfig =
+                    new SystemBarConfigBuilder()
+                            .setSide(LEFT)
+                            .setGirth(mResources.getDimensionPixelSize(
+                                    R.dimen.car_left_navigation_bar_width))
+                            .setBarType(mResources.getInteger(R.integer.config_leftSystemBarType))
+                            .setZOrder(mResources.getInteger(R.integer.config_leftSystemBarZOrder))
+                            .build();
+            mSystemBarConfigMap.put(LEFT, leftBarConfig);
+        }
+
+        if (mRightNavBarEnabled) {
+            SystemBarConfig rightBarConfig =
+                    new SystemBarConfigBuilder()
+                            .setSide(RIGHT)
+                            .setGirth(mResources.getDimensionPixelSize(
+                                    R.dimen.car_right_navigation_bar_width))
+                            .setBarType(mResources.getInteger(R.integer.config_rightSystemBarType))
+                            .setZOrder(mResources.getInteger(R.integer.config_rightSystemBarZOrder))
+                            .build();
+            mSystemBarConfigMap.put(RIGHT, rightBarConfig);
+        }
+    }
+
+    private void checkEnabledBarsHaveUniqueBarTypes() throws RuntimeException {
+        Set<Integer> barTypesUsed = new ArraySet<>();
+        int enabledNavBarCount = mSystemBarConfigMap.size();
+
+        for (SystemBarConfig systemBarConfig : mSystemBarConfigMap.values()) {
+            barTypesUsed.add(systemBarConfig.getBarType());
+        }
+
+        // The number of bar types used cannot be fewer than that of enabled system bars.
+        if (barTypesUsed.size() < enabledNavBarCount) {
+            throw new RuntimeException("Each enabled system bar must have a unique bar type. Check "
+                    + "the configuration in config.xml");
+        }
+    }
+
+    private void setInsetPaddingsForOverlappingCorners() {
+        setInsetPaddingForOverlappingCorner(TOP, LEFT);
+        setInsetPaddingForOverlappingCorner(TOP, RIGHT);
+        setInsetPaddingForOverlappingCorner(BOTTOM, LEFT);
+        setInsetPaddingForOverlappingCorner(BOTTOM, RIGHT);
+    }
+
+    private void setInsetPaddingForOverlappingCorner(@SystemBarSide int horizontalSide,
+            @SystemBarSide int verticalSide) {
+
+        if (isVerticalBar(horizontalSide) || isHorizontalBar(verticalSide)) {
+            Log.w(TAG, "configureBarPaddings: Returning immediately since the horizontal and "
+                    + "vertical sides were not provided correctly.");
+            return;
+        }
+
+        SystemBarConfig horizontalBarConfig = mSystemBarConfigMap.get(horizontalSide);
+        SystemBarConfig verticalBarConfig = mSystemBarConfigMap.get(verticalSide);
+
+        if (verticalBarConfig != null && horizontalBarConfig != null) {
+            int horizontalBarZOrder = horizontalBarConfig.getZOrder();
+            int horizontalBarGirth = horizontalBarConfig.getGirth();
+            int verticalBarZOrder = verticalBarConfig.getZOrder();
+            int verticalBarGirth = verticalBarConfig.getGirth();
+
+            if (horizontalBarZOrder > verticalBarZOrder) {
+                verticalBarConfig.setPaddingBySide(horizontalSide, horizontalBarGirth);
+            } else if (horizontalBarZOrder < verticalBarZOrder) {
+                horizontalBarConfig.setPaddingBySide(verticalSide, verticalBarGirth);
+            } else {
+                throw new RuntimeException(
+                        BAR_TITLE_MAP.get(horizontalSide) + " " + BAR_TITLE_MAP.get(verticalSide)
+                                + " have the same Z-Order, and so their placing order cannot be "
+                                + "determined. Determine which bar should be placed on top of the "
+                                + "other bar and change the Z-order in config.xml accordingly."
+                );
+            }
+        }
+    }
+
+    private void sortSystemBarSidesByZOrder() {
+        List<SystemBarConfig> systemBarsByZOrder = new ArrayList<>(mSystemBarConfigMap.values());
+
+        systemBarsByZOrder.sort(new Comparator<SystemBarConfig>() {
+            @Override
+            public int compare(SystemBarConfig o1, SystemBarConfig o2) {
+                return o1.getZOrder() - o2.getZOrder();
+            }
+        });
+
+        systemBarsByZOrder.forEach(systemBarConfig -> {
+            mSystemBarSidesByZOrder.add(systemBarConfig.getSide());
+        });
+    }
+
+    private static boolean isHorizontalBar(@SystemBarSide int side) {
+        return  side == TOP || side == BOTTOM;
+    }
+
+    private static boolean isVerticalBar(@SystemBarSide int side) {
+        return side == LEFT || side == RIGHT;
+    }
+
+    private static final class SystemBarConfig {
+        private final int mSide;
+        private final int mBarType;
+        private final int mGirth;
+        private final int mZOrder;
+
+        private int[] mPaddings = new int[]{0, 0, 0, 0};
+
+        private SystemBarConfig(@SystemBarSide int side, int barType, int girth, int zOrder) {
+            mSide = side;
+            mBarType = barType;
+            mGirth = girth;
+            mZOrder = zOrder;
+        }
+
+        private int getSide() {
+            return mSide;
+        }
+
+        private int getBarType() {
+            return mBarType;
+        }
+
+        private int getGirth() {
+            return mGirth;
+        }
+
+        private int getZOrder() {
+            return mZOrder;
+        }
+
+        private int[] getPaddings() {
+            return mPaddings;
+        }
+
+        private WindowManager.LayoutParams getLayoutParams() {
+            WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                    isHorizontalBar(mSide) ? ViewGroup.LayoutParams.MATCH_PARENT : mGirth,
+                    isHorizontalBar(mSide) ? mGirth : ViewGroup.LayoutParams.MATCH_PARENT,
+                    mapZOrderToBarType(mZOrder),
+                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                            | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+                            | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+                    PixelFormat.TRANSLUCENT);
+            lp.setTitle(BAR_TITLE_MAP.get(mSide));
+            lp.providesInsetsTypes = new int[]{BAR_TYPE_MAP[mBarType], BAR_GESTURE_MAP.get(mSide)};
+            lp.setFitInsetsTypes(0);
+            lp.windowAnimations = 0;
+            lp.gravity = BAR_GRAVITY_MAP.get(mSide);
+            return lp;
+        }
+
+        private int mapZOrderToBarType(int zOrder) {
+            return zOrder >= HUN_ZORDER ? WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL
+                    : WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL;
+        }
+
+        private void setPaddingBySide(@SystemBarSide int side, int padding) {
+            mPaddings[side] = padding;
+        }
+    }
+
+    private static final class SystemBarConfigBuilder {
+        private int mSide;
+        private int mBarType;
+        private int mGirth;
+        private int mZOrder;
+
+        private SystemBarConfigBuilder setSide(@SystemBarSide int side) {
+            mSide = side;
+            return this;
+        }
+
+        private SystemBarConfigBuilder setBarType(int type) {
+            mBarType = type;
+            return this;
+        }
+
+        private SystemBarConfigBuilder setGirth(int girth) {
+            mGirth = girth;
+            return this;
+        }
+
+        private SystemBarConfigBuilder setZOrder(int zOrder) {
+            mZOrder = zOrder;
+            return this;
+        }
+
+        private SystemBarConfig build() {
+            return new SystemBarConfig(mSide, mBarType, mGirth, mZOrder);
+        }
+    }
+}
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/CarNavigationBarControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/CarNavigationBarControllerTest.java
index dec8b8e..0b164a2 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/CarNavigationBarControllerTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/CarNavigationBarControllerTest.java
@@ -73,7 +73,8 @@
     private CarNavigationBarController createNavigationBarController() {
         return new CarNavigationBarController(mContext, mNavigationBarViewFactory,
                 mButtonSelectionStateController, () -> mHvacController,
-                mButtonRoleHolderController);
+                mButtonRoleHolderController,
+                new SystemBarConfigs(mTestableResources.getResources()));
     }
 
     @Test
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/CarNavigationBarTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/CarNavigationBarTest.java
index d9edfa96..2b5af71 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/CarNavigationBarTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/CarNavigationBarTest.java
@@ -142,7 +142,7 @@
                 mWindowManager, mDeviceProvisionedController, new CommandQueue(mContext),
                 mAutoHideController, mButtonSelectionStateListener, mHandler, mUiBgExecutor,
                 mBarService, () -> mKeyguardStateController, () -> mIconPolicy,
-                () -> mIconController);
+                () -> mIconController, new SystemBarConfigs(mTestableResources.getResources()));
     }
 
     @Test
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/SystemBarConfigsTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/SystemBarConfigsTest.java
new file mode 100644
index 0000000..8b15899
--- /dev/null
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/SystemBarConfigsTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.car.navigationbar;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Resources;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.car.CarSystemUiTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@CarSystemUiTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class SystemBarConfigsTest extends SysuiTestCase {
+
+    private SystemBarConfigs mSystemBarConfigs;
+    @Mock
+    private Resources mResources;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        setDefaultValidConfig();
+    }
+
+    @Test
+    public void onInit_allSystemBarsEnabled_eachHasUniqueBarTypes_doesNotThrowException() {
+        mSystemBarConfigs = new SystemBarConfigs(mResources);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void onInit_allSystemBarsEnabled_twoBarsHaveDuplicateType_throwsRuntimeException() {
+        when(mResources.getInteger(R.integer.config_topSystemBarType)).thenReturn(0);
+        when(mResources.getInteger(R.integer.config_bottomSystemBarType)).thenReturn(0);
+
+        mSystemBarConfigs = new SystemBarConfigs(mResources);
+    }
+
+    @Test
+    public void onInit_allSystemBarsEnabled_systemBarSidesSortedByZOrder() {
+        mSystemBarConfigs = new SystemBarConfigs(mResources);
+        List<Integer> actualOrder = mSystemBarConfigs.getSystemBarSidesByZOrder();
+        List<Integer> expectedOrder = new ArrayList<>();
+        expectedOrder.add(SystemBarConfigs.LEFT);
+        expectedOrder.add(SystemBarConfigs.RIGHT);
+        expectedOrder.add(SystemBarConfigs.TOP);
+        expectedOrder.add(SystemBarConfigs.BOTTOM);
+
+        assertTrue(actualOrder.equals(expectedOrder));
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void onInit_intersectingBarsHaveSameZOrder_throwsRuntimeException() {
+        when(mResources.getInteger(R.integer.config_topSystemBarZOrder)).thenReturn(33);
+        when(mResources.getInteger(R.integer.config_leftSystemBarZOrder)).thenReturn(33);
+
+        mSystemBarConfigs = new SystemBarConfigs(mResources);
+    }
+
+    @Test
+    public void getTopSystemBarLayoutParams_topBarEnabled_returnsTopSystemBarLayoutParams() {
+        mSystemBarConfigs = new SystemBarConfigs(mResources);
+        WindowManager.LayoutParams lp = mSystemBarConfigs.getLayoutParamsBySide(
+                SystemBarConfigs.TOP);
+
+        assertNotNull(lp);
+    }
+
+    @Test
+    public void getTopSystemBarLayoutParams_topBarNotEnabled_returnsNull() {
+        when(mResources.getBoolean(R.bool.config_enableTopNavigationBar)).thenReturn(false);
+        mSystemBarConfigs = new SystemBarConfigs(mResources);
+        WindowManager.LayoutParams lp = mSystemBarConfigs.getLayoutParamsBySide(
+                SystemBarConfigs.TOP);
+
+        assertNull(lp);
+    }
+
+    @Test
+    public void topSystemBarHasHigherZOrderThanHuns_topSystemBarIsNavigationBarPanelType() {
+        when(mResources.getInteger(R.integer.config_topSystemBarZOrder)).thenReturn(
+                SystemBarConfigs.getHunZOrder() + 1);
+        mSystemBarConfigs = new SystemBarConfigs(mResources);
+        WindowManager.LayoutParams lp = mSystemBarConfigs.getLayoutParamsBySide(
+                SystemBarConfigs.TOP);
+
+        assertEquals(lp.type, WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL);
+    }
+
+    @Test
+    public void topSystemBarHasLowerZOrderThanHuns_topSystemBarIsStatusBarAdditionalType() {
+        when(mResources.getInteger(R.integer.config_topSystemBarZOrder)).thenReturn(
+                SystemBarConfigs.getHunZOrder() - 1);
+        mSystemBarConfigs = new SystemBarConfigs(mResources);
+        WindowManager.LayoutParams lp = mSystemBarConfigs.getLayoutParamsBySide(
+                SystemBarConfigs.TOP);
+
+        assertEquals(lp.type, WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL);
+    }
+
+    // Set valid config where all system bars are enabled.
+    private void setDefaultValidConfig() {
+        when(mResources.getBoolean(R.bool.config_enableTopNavigationBar)).thenReturn(true);
+        when(mResources.getBoolean(R.bool.config_enableBottomNavigationBar)).thenReturn(true);
+        when(mResources.getBoolean(R.bool.config_enableLeftNavigationBar)).thenReturn(true);
+        when(mResources.getBoolean(R.bool.config_enableRightNavigationBar)).thenReturn(true);
+
+        when(mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.status_bar_height)).thenReturn(100);
+        when(mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.navigation_bar_height)).thenReturn(100);
+        when(mResources.getDimensionPixelSize(R.dimen.car_left_navigation_bar_width)).thenReturn(
+                100);
+        when(mResources.getDimensionPixelSize(R.dimen.car_right_navigation_bar_width)).thenReturn(
+                100);
+
+        when(mResources.getInteger(R.integer.config_topSystemBarType)).thenReturn(0);
+        when(mResources.getInteger(R.integer.config_bottomSystemBarType)).thenReturn(1);
+        when(mResources.getInteger(R.integer.config_leftSystemBarType)).thenReturn(2);
+        when(mResources.getInteger(R.integer.config_rightSystemBarType)).thenReturn(3);
+
+        when(mResources.getInteger(R.integer.config_topSystemBarZOrder)).thenReturn(5);
+        when(mResources.getInteger(R.integer.config_bottomSystemBarZOrder)).thenReturn(10);
+        when(mResources.getInteger(R.integer.config_leftSystemBarZOrder)).thenReturn(2);
+        when(mResources.getInteger(R.integer.config_rightSystemBarZOrder)).thenReturn(3);
+    }
+}
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 8e8368f..03161d0 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -659,9 +659,6 @@
     <!-- Setting Checkbox title for enabling Bluetooth Gabeldorsche. [CHAR LIMIT=40] -->
     <string name="bluetooth_enable_gabeldorsche">Enable Gabeldorsche</string>
 
-    <!-- Setting Checkbox title for enabling Enhanced Connectivity [CHAR LIMIT=80] -->
-    <string name="enhanced_connectivity">Enhanced Connectivity</string>
-
     <!-- UI debug setting: Select Bluetooth AVRCP Version -->
     <string name="bluetooth_select_avrcp_version_string">Bluetooth AVRCP Version</string>
     <!-- UI debug setting: Select Bluetooth AVRCP Version -->
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/WriteFallbackSettingsFilesJobService.java b/packages/SettingsProvider/src/com/android/providers/settings/WriteFallbackSettingsFilesJobService.java
index 6e5b889..66aa7ba 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/WriteFallbackSettingsFilesJobService.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/WriteFallbackSettingsFilesJobService.java
@@ -35,19 +35,17 @@
 public class WriteFallbackSettingsFilesJobService extends JobService {
     @Override
     public boolean onStartJob(final JobParameters params) {
-        switch (params.getJobId()) {
-            case WRITE_FALLBACK_SETTINGS_FILES_JOB_ID:
-                final List<String> settingsFiles = new ArrayList<>();
-                settingsFiles.add(params.getExtras().getString(TABLE_GLOBAL, ""));
-                settingsFiles.add(params.getExtras().getString(TABLE_SYSTEM, ""));
-                settingsFiles.add(params.getExtras().getString(TABLE_SECURE, ""));
-                settingsFiles.add(params.getExtras().getString(TABLE_SSAID, ""));
-                settingsFiles.add(params.getExtras().getString(TABLE_CONFIG, ""));
-                SettingsProvider.writeFallBackSettingsFiles(settingsFiles);
-                return true;
-            default:
-                return false;
+        if (params.getJobId() != WRITE_FALLBACK_SETTINGS_FILES_JOB_ID) {
+            return false;
         }
+        final List<String> settingsFiles = new ArrayList<>();
+        settingsFiles.add(params.getExtras().getString(TABLE_GLOBAL, ""));
+        settingsFiles.add(params.getExtras().getString(TABLE_SYSTEM, ""));
+        settingsFiles.add(params.getExtras().getString(TABLE_SECURE, ""));
+        settingsFiles.add(params.getExtras().getString(TABLE_SSAID, ""));
+        settingsFiles.add(params.getExtras().getString(TABLE_CONFIG, ""));
+        SettingsProvider.writeFallBackSettingsFiles(settingsFiles);
+        return false;
     }
 
     @Override
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 5d77a2a..e49fd6f 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -270,7 +270,6 @@
                     Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS,
                     Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS,
                     Settings.Global.ENABLE_ADB_INCREMENTAL_INSTALL_DEFAULT,
-                    Settings.Global.ENHANCED_CONNECTIVITY_ENABLED,
                     Settings.Global.ENHANCED_4G_MODE_ENABLED,
                     Settings.Global.EPHEMERAL_COOKIE_MAX_SIZE_BYTES,
                     Settings.Global.ERROR_LOGCAT_PREFIX,
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java
index eb72312..5301bbd 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java
@@ -16,44 +16,20 @@
 
 package com.android.systemui.stackdivider;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.view.Display.DEFAULT_DISPLAY;
-
 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 
 import android.app.ActivityManager;
-import android.app.ActivityTaskManager;
 import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.provider.Settings;
-import android.util.Slog;
-import android.view.LayoutInflater;
-import android.view.View;
 import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-import android.window.WindowOrganizer;
 
-import com.android.internal.policy.DividerSnapAlgorithm;
-import com.android.systemui.R;
 import com.android.systemui.SystemUI;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.wm.shell.common.DisplayChangeController;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.DisplayImeController;
-import com.android.wm.shell.common.DisplayLayout;
-import com.android.wm.shell.common.SystemWindows;
-import com.android.wm.shell.common.TransactionPool;
 
 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;
 
@@ -65,510 +41,110 @@
  * Controls the docked stack divider.
  */
 @Singleton
-public class Divider extends SystemUI implements DividerView.DividerCallbacks,
-        DisplayController.OnDisplaysChangedListener {
-    private static final String TAG = "Divider";
-
-    static final boolean DEBUG = false;
-
-    static final int DEFAULT_APP_TRANSITION_DURATION = 336;
-
+public class Divider extends SystemUI {
+    private final KeyguardStateController mKeyguardStateController;
+    private final DividerController mDividerController;
     private final Optional<Lazy<Recents>> mRecentsOptionalLazy;
 
-    private DividerWindowManager mWindowManager;
-    private DividerView mView;
-    private final DividerState mDividerState = new DividerState();
-    private boolean mVisible = false;
-    private boolean mMinimized = false;
-    private boolean mAdjustedForIme = false;
-    private boolean mHomeStackResizable = false;
-    private ForcedResizableInfoActivityController mForcedResizableController;
-    private SystemWindows mSystemWindows;
-    private DisplayController mDisplayController;
-    private DisplayImeController mImeController;
-    final TransactionPool mTransactionPool;
-
-    // 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 WindowManagerProxy mWindowManagerProxy;
-
-    private final ArrayList<WeakReference<Consumer<Boolean>>> mDockedStackExistsListeners =
-            new ArrayList<>();
-
-    private SplitScreenTaskOrganizer mSplits = new SplitScreenTaskOrganizer(this);
-
-    private DisplayChangeController.OnDisplayChangingListener mRotationController =
-            (display, fromRotation, toRotation, wct) -> {
-                if (!mSplits.isSplitScreenSupported() || mWindowManagerProxy == null) {
-                    return;
-                }
-                WindowContainerTransaction t = new WindowContainerTransaction();
-                DisplayLayout displayLayout =
-                        new DisplayLayout(mDisplayController.getDisplayLayout(display));
-                SplitDisplayLayout sdl = new SplitDisplayLayout(mContext, displayLayout, mSplits);
-                sdl.rotateTo(toRotation);
-                mRotateSplitLayout = sdl;
-                final int position = isDividerVisible()
-                        ? (mMinimized ? mView.mSnapTargetBeforeMinimized.position
-                                : mView.getCurrentPosition())
-                        // snap resets to middle target when not in split-mode
-                        : sdl.getSnapAlgorithm().getMiddleTarget().position;
-                DividerSnapAlgorithm snap = sdl.getSnapAlgorithm();
-                final DividerSnapAlgorithm.SnapTarget target =
-                        snap.calculateNonDismissingSnapTarget(position);
-                sdl.resizeSplits(target.position, t);
-
-                if (isSplitActive() && mHomeStackResizable) {
-                    WindowManagerProxy.applyHomeTasksMinimized(sdl, mSplits.mSecondary.token, t);
-                }
-                if (mWindowManagerProxy.queueSyncTransactionIfWaiting(t)) {
-                    // Because sync transactions are serialized, its possible for an "older"
-                    // bounds-change to get applied after a screen rotation. In that case, we
-                    // want to actually defer on that rather than apply immediately. Of course,
-                    // this means that the bounds may not change until after the rotation so
-                    // the user might see some artifacts. This should be rare.
-                    Slog.w(TAG, "Screen rotated while other operations were pending, this may"
-                            + " result in some graphical artifacts.");
-                } else {
-                    wct.merge(t, true /* transfer */);
-                }
-            };
-
-    private final DividerImeController mImePositionProcessor;
-
-    private TaskStackChangeListener mActivityRestartListener = new TaskStackChangeListener() {
-        @Override
-        public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
-                boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
-            if (!wasVisible || task.configuration.windowConfiguration.getWindowingMode()
-                    != WINDOWING_MODE_SPLIT_SCREEN_PRIMARY || !mSplits.isSplitScreenSupported()) {
-                return;
-            }
-
-            if (isMinimized()) {
-                onUndockingTask();
-            }
-        }
-    };
-
-    public Divider(Context context, Optional<Lazy<Recents>> recentsOptionalLazy,
-            DisplayController displayController, SystemWindows systemWindows,
-            DisplayImeController imeController, Handler handler,
-            KeyguardStateController keyguardStateController, TransactionPool transactionPool) {
+    Divider(Context context, DividerController dividerController,
+            KeyguardStateController keyguardStateController,
+            Optional<Lazy<Recents>> recentsOptionalLazy) {
         super(context);
-        mDisplayController = displayController;
-        mSystemWindows = systemWindows;
-        mImeController = imeController;
-        mHandler = handler;
+        mDividerController = dividerController;
         mKeyguardStateController = keyguardStateController;
         mRecentsOptionalLazy = recentsOptionalLazy;
-        mForcedResizableController = new ForcedResizableInfoActivityController(context, this);
-        mTransactionPool = transactionPool;
-        mWindowManagerProxy = new WindowManagerProxy(mTransactionPool, mHandler);
-        mImePositionProcessor = new DividerImeController(mSplits, mTransactionPool, mHandler);
     }
 
     @Override
     public void start() {
-        mWindowManager = new DividerWindowManager(mSystemWindows);
-        mDisplayController.addDisplayWindowListener(this);
+        mDividerController.start();
         // 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 (!isSplitActive() || mView == null) {
-                    return;
-                }
-                mView.setHidden(mKeyguardStateController.isShowing());
-                if (!mKeyguardStateController.isShowing()) {
-                    mImePositionProcessor.updateAdjustForIme();
-                }
-            }
-
-            @Override
-            public void onKeyguardFadingAwayChanged() {
-
+                mDividerController.onKeyguardShowingChanged(mKeyguardStateController.isShowing());
             }
         });
         // Don't initialize the divider or anything until we get the default display.
-    }
 
-    @Override
-    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);
-        if (!ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)) {
-            removeDivider();
-            return;
-        }
-        try {
-            mSplits.init();
-            // Set starting tile bounds based on middle target
-            final WindowContainerTransaction tct = new WindowContainerTransaction();
-            int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
-            mSplitLayout.resizeSplits(midPos, tct);
-            WindowOrganizer.applyTransaction(tct);
-        } catch (Exception e) {
-            Slog.e(TAG, "Failed to register docked stack listener", e);
-            removeDivider();
-            return;
-        }
-        ActivityManagerWrapper.getInstance().registerTaskStackListener(mActivityRestartListener);
-    }
+        ActivityManagerWrapper.getInstance().registerTaskStackListener(
+                new TaskStackChangeListener() {
+                    @Override
+                    public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
+                            boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
+                        if (!wasVisible || task.configuration.windowConfiguration.getWindowingMode()
+                                != WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
+                                || !mDividerController.isSplitScreenSupported()) {
+                            return;
+                        }
 
-    @Override
-    public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
-        if (displayId != DEFAULT_DISPLAY || !mSplits.isSplitScreenSupported()) {
-            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);
-            WindowOrganizer.applyTransaction(tct);
-        } else if (mSplitLayout.mDisplayLayout.rotation()
-                        == mRotateSplitLayout.mDisplayLayout.rotation()) {
-            mSplitLayout.mPrimary = new Rect(mRotateSplitLayout.mPrimary);
-            mSplitLayout.mSecondary = new Rect(mRotateSplitLayout.mSecondary);
-            mRotateSplitLayout = null;
-        }
-        if (isSplitActive()) {
-            update(newConfig);
-        }
-    }
-
-    Handler getHandler() {
-        return mHandler;
-    }
-
-    public DividerView getView() {
-        return mView;
-    }
-
-    public boolean isMinimized() {
-        return mMinimized;
-    }
-
-    public boolean isHomeStackResizable() {
-        return mHomeStackResizable;
-    }
-
-    /** {@code true} if this is visible */
-    public boolean isDividerVisible() {
-        return mView != null && mView.getVisibility() == View.VISIBLE;
-    }
-
-    /**
-     * This indicates that at-least one of the splits has content. This differs from
-     * isDividerVisible because the divider is only visible once *everything* is in split mode
-     * while this only cares if some things are (eg. while entering/exiting as well).
-     */
-    private boolean isSplitActive() {
-        return mSplits.mPrimary != null && mSplits.mSecondary != null
-                && (mSplits.mPrimary.topActivityType != ACTIVITY_TYPE_UNDEFINED
-                        || mSplits.mSecondary.topActivityType != ACTIVITY_TYPE_UNDEFINED);
-    }
-
-    private void addDivider(Configuration configuration) {
-        Context dctx = mDisplayController.getDisplayContext(mContext.getDisplayId());
-        mView = (DividerView)
-                LayoutInflater.from(dctx).inflate(R.layout.docked_stack_divider, null);
-        DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mContext.getDisplayId());
-        mView.injectDependencies(mWindowManager, mDividerState, this, mSplits, mSplitLayout,
-                mImePositionProcessor, mWindowManagerProxy);
-        mView.setVisibility(mVisible ? View.VISIBLE : View.INVISIBLE);
-        mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, null /* transaction */);
-        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 : displayLayout.width();
-        final int height = landscape ? displayLayout.height() : size;
-        mWindowManager.add(mView, width, height, mContext.getDisplayId());
-    }
-
-    private void removeDivider() {
-        if (mView != null) {
-            mView.onDividerRemoved();
-        }
-        mWindowManager.remove();
-    }
-
-    private void update(Configuration configuration) {
-        final boolean isDividerHidden = mView != null && mKeyguardStateController.isShowing();
-
-        removeDivider();
-        addDivider(configuration);
-
-        if (mMinimized) {
-            mView.setMinimizedDockStack(true, mHomeStackResizable, null /* transaction */);
-            updateTouchable();
-        }
-        mView.setHidden(isDividerHidden);
-    }
-
-    void onTaskVanished() {
-        mHandler.post(this::removeDivider);
-    }
-
-    private void updateVisibility(final boolean visible) {
-        if (DEBUG) Slog.d(TAG, "Updating visibility " + mVisible + "->" + visible);
-        if (mVisible != visible) {
-            mVisible = visible;
-            mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
-
-            if (visible) {
-                mView.enterSplitMode(mHomeStackResizable);
-                // Update state because animations won't finish.
-                mWindowManagerProxy.runInSync(
-                        t -> mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, t));
-
-            } else {
-                mView.exitSplitMode();
-                mWindowManagerProxy.runInSync(
-                        t -> mView.setMinimizedDockStack(false, mHomeStackResizable, t));
-            }
-            // Notify existence listeners
-            synchronized (mDockedStackExistsListeners) {
-                mDockedStackExistsListeners.removeIf(wf -> {
-                    Consumer<Boolean> l = wf.get();
-                    if (l != null) l.accept(visible);
-                    return l == null;
-                });
-            }
-        }
-    }
-
-    /** Switch to minimized state if appropriate */
-    public void setMinimized(final boolean minimized) {
-        if (DEBUG) Slog.d(TAG, "posting ext setMinimized " + minimized + " vis:" + mVisible);
-        mHandler.post(() -> {
-            if (DEBUG) Slog.d(TAG, "run posted ext setMinimized " + minimized + " vis:" + mVisible);
-            if (!mVisible) {
-                return;
-            }
-            setHomeMinimized(minimized, mHomeStackResizable);
-        });
-    }
-
-    private void setHomeMinimized(final boolean minimized, boolean homeStackResizable) {
-        if (DEBUG) {
-            Slog.d(TAG, "setHomeMinimized  min:" + mMinimized + "->" + minimized + " hrsz:"
-                    + mHomeStackResizable + "->" + homeStackResizable
-                    + " split:" + isDividerVisible());
-        }
-        WindowContainerTransaction wct = new WindowContainerTransaction();
-        final boolean minimizedChanged = mMinimized != minimized;
-        // Update minimized state
-        if (minimizedChanged) {
-            mMinimized = minimized;
-        }
-        // Always set this because we could be entering split when mMinimized is already true
-        wct.setFocusable(mSplits.mPrimary.token, !mMinimized);
-        boolean onlyFocusable = true;
-
-        // Update home-stack resizability
-        final boolean homeResizableChanged = mHomeStackResizable != homeStackResizable;
-        if (homeResizableChanged) {
-            mHomeStackResizable = homeStackResizable;
-            if (isDividerVisible()) {
-                WindowManagerProxy.applyHomeTasksMinimized(
-                        mSplitLayout, mSplits.mSecondary.token, wct);
-                onlyFocusable = false;
-            }
-        }
-
-        // Sync state to DividerView if it exists.
-        if (mView != null) {
-            final int displayId = mView.getDisplay() != null
-                    ? mView.getDisplay().getDisplayId() : DEFAULT_DISPLAY;
-            // pause ime here (before updateMinimizedDockedStack)
-            if (mMinimized) {
-                mImePositionProcessor.pause(displayId);
-            }
-            if (minimizedChanged || homeResizableChanged) {
-                // This conflicts with IME adjustment, so only call it when things change.
-                mView.setMinimizedDockStack(minimized, getAnimDuration(), homeStackResizable);
-            }
-            if (!mMinimized) {
-                // afterwards so it can end any animations started in view
-                mImePositionProcessor.resume(displayId);
-            }
-        }
-        updateTouchable();
-        if (onlyFocusable) {
-            // If we are only setting focusability, a sync transaction isn't necessary (in fact it
-            // can interrupt other animations), so see if it can be submitted on pending instead.
-            if (!mSplits.mDivider.getWmProxy().queueSyncTransactionIfWaiting(wct)) {
-                WindowOrganizer.applyTransaction(wct);
-            }
-        } else {
-            mWindowManagerProxy.applySyncTransaction(wct);
-        }
-    }
-
-    void setAdjustedForIme(boolean adjustedForIme) {
-        if (mAdjustedForIme == adjustedForIme) {
-            return;
-        }
-        mAdjustedForIme = adjustedForIme;
-        updateTouchable();
-    }
-
-    private void updateTouchable() {
-        mWindowManager.setTouchable(!mAdjustedForIme);
-    }
-
-    /**
-     * 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
-     * subscriber, or DividerView, which has been removed and prevented from resizing. Instead,
-     * register the event handler here and proxy the event to the current DividerView.
-     */
-    public void onRecentsDrawn() {
-        if (mView != null) {
-            mView.onRecentsDrawn();
-        }
-    }
-
-    public void onUndockingTask() {
-        if (mView != null) {
-            mView.onUndockingTask();
-        }
-    }
-
-    public void onDockedFirstAnimationFrame() {
-        if (mView != null) {
-            mView.onDockedFirstAnimationFrame();
-        }
-    }
-
-    public void onDockedTopTask() {
-        if (mView != null) {
-            mView.onDockedTopTask();
-        }
-    }
-
-    public void onAppTransitionFinished() {
-        if (mView == null) {
-            return;
-        }
-        mForcedResizableController.onAppTransitionFinished();
-    }
-
-    @Override
-    public void onDraggingStart() {
-        mForcedResizableController.onDraggingStart();
-    }
-
-    @Override
-    public void onDraggingEnd() {
-        mForcedResizableController.onDraggingEnd();
-    }
-
-    @Override
-    public void growRecents() {
-        mRecentsOptionalLazy.ifPresent(recentsLazy -> recentsLazy.get().growRecents());
+                        if (mDividerController.isMinimized()) {
+                            onUndockingTask();
+                        }
+                    }
+                }
+        );
     }
 
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        pw.print("  mVisible="); pw.println(mVisible);
-        pw.print("  mMinimized="); pw.println(mMinimized);
-        pw.print("  mAdjustedForIme="); pw.println(mAdjustedForIme);
+        mDividerController.dump(pw);
     }
 
-    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);
+    /** Switch to minimized state if appropriate. */
+    public void setMinimized(final boolean minimized) {
+        mDividerController.setMinimized(minimized);
     }
 
-    /** Register a listener that gets called whenever the existence of the divider changes */
-    public void registerInSplitScreenListener(Consumer<Boolean> listener) {
-        listener.accept(isDividerVisible());
-        synchronized (mDockedStackExistsListeners) {
-            mDockedStackExistsListeners.add(new WeakReference<>(listener));
-        }
+    public boolean isMinimized() {
+        return mDividerController.isMinimized();
     }
 
-    void startEnterSplit() {
-        update(mDisplayController.getDisplayContext(
-                mContext.getDisplayId()).getResources().getConfiguration());
-        // Set resizable directly here because applyEnterSplit already resizes home stack.
-        mHomeStackResizable = mWindowManagerProxy.applyEnterSplit(mSplits, mSplitLayout);
+    public boolean isHomeStackResizable() {
+        return mDividerController.isHomeStackResizable();
     }
 
-    void startDismissSplit() {
-        mWindowManagerProxy.applyDismissSplit(mSplits, mSplitLayout, true /* dismissOrMaximize */);
-        updateVisibility(false /* visible */);
-        mMinimized = false;
-        removeDivider();
-        mImePositionProcessor.reset();
+    /** Callback for undocking task. */
+    public void onUndockingTask() {
+        mDividerController.onUndockingTask();
     }
 
-    void ensureMinimizedSplit() {
-        setHomeMinimized(true /* minimized */, mHomeStackResizable);
-        if (mView != null && !isDividerVisible()) {
-            // Wasn't in split-mode yet, so enter now.
-            if (DEBUG) {
-                Slog.d(TAG, " entering split mode with minimized=true");
-            }
-            updateVisibility(true /* visible */);
-        }
+    public void onRecentsDrawn() {
+        mDividerController.onRecentsDrawn(() -> mRecentsOptionalLazy.ifPresent(
+                recentsLazy -> recentsLazy.get().growRecents()));
     }
 
-    void ensureNormalSplit() {
-        setHomeMinimized(false /* minimized */, mHomeStackResizable);
-        if (mView != null && !isDividerVisible()) {
-            // Wasn't in split-mode, so enter now.
-            if (DEBUG) {
-                Slog.d(TAG, " enter split mode unminimized ");
-            }
-            updateVisibility(true /* visible */);
-        }
+    public void onDockedFirstAnimationFrame() {
+        mDividerController.onDockedFirstAnimationFrame();
     }
 
-    SplitDisplayLayout getSplitLayout() {
-        return mSplitLayout;
+    public void onDockedTopTask() {
+        mDividerController.onDockedTopTask();
     }
 
-    WindowManagerProxy getWmProxy() {
-        return mWindowManagerProxy;
+    public void onAppTransitionFinished() {
+        mDividerController.onAppTransitionFinished();
+    }
+
+    public DividerView getView() {
+        return mDividerController.getDividerView();
     }
 
     /** @return the container token for the secondary split root task. */
     public WindowContainerToken getSecondaryRoot() {
-        if (mSplits == null || mSplits.mSecondary == null) {
-            return null;
-        }
-        return mSplits.mSecondary.token;
+        return mDividerController.getSecondaryRoot();
+    }
+
+    /** Register a listener that gets called whenever the existence of the divider changes */
+    public void registerInSplitScreenListener(Consumer<Boolean> listener) {
+        mDividerController.registerInSplitScreenListener(listener);
+    }
+
+    /** {@code true} if this is visible */
+    public boolean isDividerVisible() {
+        return mDividerController.isDividerVisible();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerController.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerController.java
new file mode 100644
index 0000000..14fc157
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerController.java
@@ -0,0 +1,516 @@
+/*
+ * 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_UNDEFINED;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+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.provider.Settings;
+import android.util.Slog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import android.window.WindowOrganizer;
+
+import com.android.internal.policy.DividerSnapAlgorithm;
+import com.android.systemui.R;
+import com.android.wm.shell.common.DisplayChangeController;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.SystemWindows;
+import com.android.wm.shell.common.TransactionPool;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+/**
+ * Controls the docked stack divider.
+ */
+public class DividerController implements DividerView.DividerCallbacks,
+        DisplayController.OnDisplaysChangedListener {
+    static final boolean DEBUG = false;
+    private static final String TAG = "Divider";
+
+    static final int DEFAULT_APP_TRANSITION_DURATION = 336;
+
+    private DividerWindowManager mWindowManager;
+    private DividerView mView;
+    private final DividerState mDividerState = new DividerState();
+    private boolean mVisible = false;
+    private boolean mMinimized = false;
+    private boolean mAdjustedForIme = false;
+    private boolean mHomeStackResizable = false;
+    private ForcedResizableInfoActivityController mForcedResizableController;
+    private SystemWindows mSystemWindows;
+    private DisplayController mDisplayController;
+    private DisplayImeController mImeController;
+    final TransactionPool mTransactionPool;
+
+    // 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 final Handler mHandler;
+    private final WindowManagerProxy mWindowManagerProxy;
+
+    private final ArrayList<WeakReference<Consumer<Boolean>>> mDockedStackExistsListeners =
+            new ArrayList<>();
+
+    private final SplitScreenTaskOrganizer mSplits;
+    private final DisplayChangeController.OnDisplayChangingListener mRotationController;
+    private final DividerImeController mImePositionProcessor;
+    private final Context mContext;
+    private boolean mIsKeyguardShowing;
+
+    public DividerController(Context context,
+            DisplayController displayController, SystemWindows systemWindows,
+            DisplayImeController imeController, Handler handler, TransactionPool transactionPool) {
+        mContext = context;
+        mDisplayController = displayController;
+        mSystemWindows = systemWindows;
+        mImeController = imeController;
+        mHandler = handler;
+        mForcedResizableController = new ForcedResizableInfoActivityController(context, this);
+        mTransactionPool = transactionPool;
+        mWindowManagerProxy = new WindowManagerProxy(mTransactionPool, mHandler);
+        mSplits = new SplitScreenTaskOrganizer(this);
+        mImePositionProcessor = new DividerImeController(mSplits, mTransactionPool, mHandler);
+        mRotationController =
+                (display, fromRotation, toRotation, wct) -> {
+                    if (!mSplits.isSplitScreenSupported() || mWindowManagerProxy == null) {
+                        return;
+                    }
+                    WindowContainerTransaction t = new WindowContainerTransaction();
+                    DisplayLayout displayLayout =
+                            new DisplayLayout(mDisplayController.getDisplayLayout(display));
+                    SplitDisplayLayout sdl =
+                            new SplitDisplayLayout(mContext, displayLayout, mSplits);
+                    sdl.rotateTo(toRotation);
+                    mRotateSplitLayout = sdl;
+                    final int position = isDividerVisible()
+                            ? (mMinimized ? mView.mSnapTargetBeforeMinimized.position
+                            : mView.getCurrentPosition())
+                            // snap resets to middle target when not in split-mode
+                            : sdl.getSnapAlgorithm().getMiddleTarget().position;
+                    DividerSnapAlgorithm snap = sdl.getSnapAlgorithm();
+                    final DividerSnapAlgorithm.SnapTarget target =
+                            snap.calculateNonDismissingSnapTarget(position);
+                    sdl.resizeSplits(target.position, t);
+
+                    if (isSplitActive() && mHomeStackResizable) {
+                        WindowManagerProxy
+                                .applyHomeTasksMinimized(sdl, mSplits.mSecondary.token, t);
+                    }
+                    if (mWindowManagerProxy.queueSyncTransactionIfWaiting(t)) {
+                        // Because sync transactions are serialized, its possible for an "older"
+                        // bounds-change to get applied after a screen rotation. In that case, we
+                        // want to actually defer on that rather than apply immediately. Of course,
+                        // this means that the bounds may not change until after the rotation so
+                        // the user might see some artifacts. This should be rare.
+                        Slog.w(TAG, "Screen rotated while other operations were pending, this may"
+                                + " result in some graphical artifacts.");
+                    } else {
+                        wct.merge(t, true /* transfer */);
+                    }
+                };
+    }
+
+    /** Inits the divider service. */
+    public void start() {
+        mWindowManager = new DividerWindowManager(mSystemWindows);
+        mDisplayController.addDisplayWindowListener(this);
+        // Don't initialize the divider or anything until we get the default display.
+    }
+
+    /** Returns {@code true} if split screen is supported on the device. */
+    public boolean isSplitScreenSupported() {
+        return mSplits.isSplitScreenSupported();
+    }
+
+    /** Called when keyguard showing state changed. */
+    public void onKeyguardShowingChanged(boolean isShowing) {
+        if (!isSplitActive() || mView == null) {
+            return;
+        }
+        mView.setHidden(isShowing);
+        if (!isShowing) {
+            mImePositionProcessor.updateAdjustForIme();
+        }
+        mIsKeyguardShowing = isShowing;
+    }
+
+    @Override
+    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);
+        if (!ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)) {
+            removeDivider();
+            return;
+        }
+        try {
+            mSplits.init();
+            // Set starting tile bounds based on middle target
+            final WindowContainerTransaction tct = new WindowContainerTransaction();
+            int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
+            mSplitLayout.resizeSplits(midPos, tct);
+            WindowOrganizer.applyTransaction(tct);
+        } catch (Exception e) {
+            Slog.e(TAG, "Failed to register docked stack listener", e);
+            removeDivider();
+            return;
+        }
+    }
+
+    @Override
+    public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
+        if (displayId != DEFAULT_DISPLAY || !mSplits.isSplitScreenSupported()) {
+            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);
+            WindowOrganizer.applyTransaction(tct);
+        } else if (mSplitLayout.mDisplayLayout.rotation()
+                == mRotateSplitLayout.mDisplayLayout.rotation()) {
+            mSplitLayout.mPrimary = new Rect(mRotateSplitLayout.mPrimary);
+            mSplitLayout.mSecondary = new Rect(mRotateSplitLayout.mSecondary);
+            mRotateSplitLayout = null;
+        }
+        if (isSplitActive()) {
+            update(newConfig);
+        }
+    }
+
+    /** Posts task to handler dealing with divider. */
+    void post(Runnable task) {
+        mHandler.post(task);
+    }
+
+    /** Returns {@link DividerView}. */
+    public DividerView getDividerView() {
+        return mView;
+    }
+
+    /** Returns {@code true} if one of the split screen is in minimized mode. */
+    public boolean isMinimized() {
+        return mMinimized;
+    }
+
+    public boolean isHomeStackResizable() {
+        return mHomeStackResizable;
+    }
+
+    /** Returns {@code true} if the divider is visible. */
+    public boolean isDividerVisible() {
+        return mView != null && mView.getVisibility() == View.VISIBLE;
+    }
+
+    /**
+     * This indicates that at-least one of the splits has content. This differs from
+     * isDividerVisible because the divider is only visible once *everything* is in split mode
+     * while this only cares if some things are (eg. while entering/exiting as well).
+     */
+    private boolean isSplitActive() {
+        return mSplits.mPrimary != null && mSplits.mSecondary != null
+                && (mSplits.mPrimary.topActivityType != ACTIVITY_TYPE_UNDEFINED
+                || mSplits.mSecondary.topActivityType != ACTIVITY_TYPE_UNDEFINED);
+    }
+
+    private void addDivider(Configuration configuration) {
+        Context dctx = mDisplayController.getDisplayContext(mContext.getDisplayId());
+        mView = (DividerView)
+                LayoutInflater.from(dctx).inflate(R.layout.docked_stack_divider, null);
+        DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mContext.getDisplayId());
+        mView.injectDependencies(mWindowManager, mDividerState, this, mSplits, mSplitLayout,
+                mImePositionProcessor, mWindowManagerProxy);
+        mView.setVisibility(mVisible ? View.VISIBLE : View.INVISIBLE);
+        mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, null /* transaction */);
+        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 : displayLayout.width();
+        final int height = landscape ? displayLayout.height() : size;
+        mWindowManager.add(mView, width, height, mContext.getDisplayId());
+    }
+
+    private void removeDivider() {
+        if (mView != null) {
+            mView.onDividerRemoved();
+        }
+        mWindowManager.remove();
+    }
+
+    private void update(Configuration configuration) {
+        final boolean isDividerHidden = mView != null && mIsKeyguardShowing;
+
+        removeDivider();
+        addDivider(configuration);
+
+        if (mMinimized) {
+            mView.setMinimizedDockStack(true, mHomeStackResizable, null /* transaction */);
+            updateTouchable();
+        }
+        mView.setHidden(isDividerHidden);
+    }
+
+    void onTaskVanished() {
+        mHandler.post(this::removeDivider);
+    }
+
+    private void updateVisibility(final boolean visible) {
+        if (DEBUG) Slog.d(TAG, "Updating visibility " + mVisible + "->" + visible);
+        if (mVisible != visible) {
+            mVisible = visible;
+            mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+
+            if (visible) {
+                mView.enterSplitMode(mHomeStackResizable);
+                // Update state because animations won't finish.
+                mWindowManagerProxy.runInSync(
+                        t -> mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, t));
+
+            } else {
+                mView.exitSplitMode();
+                mWindowManagerProxy.runInSync(
+                        t -> mView.setMinimizedDockStack(false, mHomeStackResizable, t));
+            }
+            // Notify existence listeners
+            synchronized (mDockedStackExistsListeners) {
+                mDockedStackExistsListeners.removeIf(wf -> {
+                    Consumer<Boolean> l = wf.get();
+                    if (l != null) l.accept(visible);
+                    return l == null;
+                });
+            }
+        }
+    }
+
+    /** Switch to minimized state if appropriate. */
+    public void setMinimized(final boolean minimized) {
+        if (DEBUG) Slog.d(TAG, "posting ext setMinimized " + minimized + " vis:" + mVisible);
+        mHandler.post(() -> {
+            if (DEBUG) Slog.d(TAG, "run posted ext setMinimized " + minimized + " vis:" + mVisible);
+            if (!mVisible) {
+                return;
+            }
+            setHomeMinimized(minimized);
+        });
+    }
+
+    private void setHomeMinimized(final boolean minimized) {
+        if (DEBUG) {
+            Slog.d(TAG, "setHomeMinimized  min:" + mMinimized + "->" + minimized + " hrsz:"
+                    + mHomeStackResizable + " split:" + isDividerVisible());
+        }
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        final boolean minimizedChanged = mMinimized != minimized;
+        // Update minimized state
+        if (minimizedChanged) {
+            mMinimized = minimized;
+        }
+        // Always set this because we could be entering split when mMinimized is already true
+        wct.setFocusable(mSplits.mPrimary.token, !mMinimized);
+
+        // Sync state to DividerView if it exists.
+        if (mView != null) {
+            final int displayId = mView.getDisplay() != null
+                    ? mView.getDisplay().getDisplayId() : DEFAULT_DISPLAY;
+            // pause ime here (before updateMinimizedDockedStack)
+            if (mMinimized) {
+                mImePositionProcessor.pause(displayId);
+            }
+            if (minimizedChanged) {
+                // This conflicts with IME adjustment, so only call it when things change.
+                mView.setMinimizedDockStack(minimized, getAnimDuration(), mHomeStackResizable);
+            }
+            if (!mMinimized) {
+                // afterwards so it can end any animations started in view
+                mImePositionProcessor.resume(displayId);
+            }
+        }
+        updateTouchable();
+
+        // If we are only setting focusability, a sync transaction isn't necessary (in fact it
+        // can interrupt other animations), so see if it can be submitted on pending instead.
+        if (!mWindowManagerProxy.queueSyncTransactionIfWaiting(wct)) {
+            WindowOrganizer.applyTransaction(wct);
+        }
+    }
+
+    void setAdjustedForIme(boolean adjustedForIme) {
+        if (mAdjustedForIme == adjustedForIme) {
+            return;
+        }
+        mAdjustedForIme = adjustedForIme;
+        updateTouchable();
+    }
+
+    private void updateTouchable() {
+        mWindowManager.setTouchable(!mAdjustedForIme);
+    }
+
+    /**
+     * 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
+     * subscriber, or DividerView, which has been removed and prevented from resizing. Instead,
+     * register the event handler here and proxy the event to the current DividerView.
+     */
+    public void onRecentsDrawn(DividerView.RecentDrawnCallback callback) {
+        if (mView != null) {
+            mView.onRecentsDrawn(callback);
+        }
+    }
+
+    /** Called when there's a task undocking.  */
+    public void onUndockingTask() {
+        if (mView != null) {
+            mView.onUndockingTask();
+        }
+    }
+
+    /** Called when the first docked animation frame rendered. */
+    public void onDockedFirstAnimationFrame() {
+        if (mView != null) {
+            mView.onDockedFirstAnimationFrame();
+        }
+    }
+
+    /** Called when top task docked. */
+    public void onDockedTopTask() {
+        if (mView != null) {
+            mView.onDockedTopTask();
+        }
+    }
+
+    /** Called when app transition finished. */
+    public void onAppTransitionFinished() {
+        if (mView == null) {
+            return;
+        }
+        mForcedResizableController.onAppTransitionFinished();
+    }
+
+    @Override
+    public void onDraggingStart() {
+        mForcedResizableController.onDraggingStart();
+    }
+
+    @Override
+    public void onDraggingEnd() {
+        mForcedResizableController.onDraggingEnd();
+    }
+
+    /** Dumps current status of Divider.*/
+    public void dump(PrintWriter pw) {
+        pw.print("  mVisible="); pw.println(mVisible);
+        pw.print("  mMinimized="); pw.println(mMinimized);
+        pw.print("  mAdjustedForIme="); pw.println(mAdjustedForIme);
+    }
+
+    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);
+    }
+
+    /** Registers listener that gets called whenever the existence of the divider changes. */
+    public void registerInSplitScreenListener(Consumer<Boolean> listener) {
+        listener.accept(isDividerVisible());
+        synchronized (mDockedStackExistsListeners) {
+            mDockedStackExistsListeners.add(new WeakReference<>(listener));
+        }
+    }
+
+    void startEnterSplit() {
+        update(mDisplayController.getDisplayContext(
+                mContext.getDisplayId()).getResources().getConfiguration());
+        // Set resizable directly here because applyEnterSplit already resizes home stack.
+        mHomeStackResizable = mWindowManagerProxy.applyEnterSplit(mSplits, mSplitLayout);
+    }
+
+    void startDismissSplit() {
+        mWindowManagerProxy.applyDismissSplit(mSplits, mSplitLayout, true /* dismissOrMaximize */);
+        updateVisibility(false /* visible */);
+        mMinimized = false;
+        removeDivider();
+        mImePositionProcessor.reset();
+    }
+
+    void ensureMinimizedSplit() {
+        setHomeMinimized(true /* minimized */);
+        if (mView != null && !isDividerVisible()) {
+            // Wasn't in split-mode yet, so enter now.
+            if (DEBUG) {
+                Slog.d(TAG, " entering split mode with minimized=true");
+            }
+            updateVisibility(true /* visible */);
+        }
+    }
+
+    void ensureNormalSplit() {
+        setHomeMinimized(false /* minimized */);
+        if (mView != null && !isDividerVisible()) {
+            // Wasn't in split-mode, so enter now.
+            if (DEBUG) {
+                Slog.d(TAG, " enter split mode unminimized ");
+            }
+            updateVisibility(true /* visible */);
+        }
+    }
+
+    SplitDisplayLayout getSplitLayout() {
+        return mSplitLayout;
+    }
+
+    WindowManagerProxy getWmProxy() {
+        return mWindowManagerProxy;
+    }
+
+    /** @return the container token for the secondary split root task. */
+    public WindowContainerToken getSecondaryRoot() {
+        if (mSplits == null || mSplits.mSecondary == null) {
+            return null;
+        }
+        return mSplits.mSecondary.token;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerHandleView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerHandleView.java
index d5f7b39..a10242a 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerHandleView.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerHandleView.java
@@ -34,7 +34,7 @@
 /**
  * View for the handle in the docked stack divider.
  */
-public class DividerHandleView extends View {
+class DividerHandleView extends View {
 
     private final static Property<DividerHandleView, Integer> WIDTH_PROPERTY
             = new Property<DividerHandleView, Integer>(Integer.class, "width") {
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerImeController.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerImeController.java
index 84ec387..c915f07 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerImeController.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerImeController.java
@@ -38,7 +38,7 @@
 
 class DividerImeController implements DisplayImeController.ImePositionProcessor {
     private static final String TAG = "DividerImeController";
-    private static final boolean DEBUG = Divider.DEBUG;
+    private static final boolean DEBUG = DividerController.DEBUG;
 
     private static final float ADJUSTED_NONFOCUS_DIM = 0.3f;
 
@@ -100,15 +100,15 @@
     }
 
     private DividerView getView() {
-        return mSplits.mDivider.getView();
+        return mSplits.mDividerController.getDividerView();
     }
 
     private SplitDisplayLayout getLayout() {
-        return mSplits.mDivider.getSplitLayout();
+        return mSplits.mDividerController.getSplitLayout();
     }
 
     private boolean isDividerVisible() {
-        return mSplits.mDivider.isDividerVisible();
+        return mSplits.mDividerController.isDividerVisible();
     }
 
     private boolean getSecondaryHasFocus(int displayId) {
@@ -151,7 +151,7 @@
         mSecondaryHasFocus = getSecondaryHasFocus(displayId);
         final boolean targetAdjusted = splitIsVisible && imeShouldShow && mSecondaryHasFocus
                 && !imeIsFloating && !getLayout().mDisplayLayout.isLandscape()
-                && !mSplits.mDivider.isMinimized();
+                && !mSplits.mDividerController.isMinimized();
         if (mLastAdjustTop < 0) {
             mLastAdjustTop = imeShouldShow ? hiddenTop : shownTop;
         } else if (mLastAdjustTop != (imeShouldShow ? mShownTop : mHiddenTop)) {
@@ -236,7 +236,7 @@
                         SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
             }
 
-            if (!mSplits.mDivider.getWmProxy().queueSyncTransactionIfWaiting(wct)) {
+            if (!mSplits.mDividerController.getWmProxy().queueSyncTransactionIfWaiting(wct)) {
                 WindowOrganizer.applyTransaction(wct);
             }
         }
@@ -250,7 +250,7 @@
                         : DisplayImeController.ANIMATION_DURATION_HIDE_MS);
             }
         }
-        mSplits.mDivider.setAdjustedForIme(mTargetShown && !mPaused);
+        mSplits.mDividerController.setAdjustedForIme(mTargetShown && !mPaused);
     }
 
     public void updateAdjustForIme() {
@@ -343,10 +343,12 @@
         mAnimation.setInterpolator(DisplayImeController.INTERPOLATOR);
         mAnimation.addListener(new AnimatorListenerAdapter() {
             private boolean mCancel = false;
+
             @Override
             public void onAnimationCancel(Animator animation) {
                 mCancel = true;
             }
+
             @Override
             public void onAnimationEnd(Animator animation) {
                 SurfaceControl.Transaction t = mTransactionPool.acquire();
@@ -400,7 +402,8 @@
             mTargetAdjusted = mPausedTargetAdjusted;
             updateDimTargets();
             final DividerView view = getView();
-            if ((mTargetAdjusted != mAdjusted) && !mSplits.mDivider.isMinimized() && view != null) {
+            if ((mTargetAdjusted != mAdjusted) && !mSplits.mDividerController.isMinimized()
+                    && view != null) {
                 // End unminimize animations since they conflict with adjustment animations.
                 view.finishAnimations();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerModule.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerModule.java
index c24431c..cdf44d7 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerModule.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerModule.java
@@ -46,7 +46,10 @@
             DisplayController displayController, SystemWindows systemWindows,
             DisplayImeController imeController, @Main Handler handler,
             KeyguardStateController keyguardStateController, TransactionPool transactionPool) {
-        return new Divider(context, recentsOptionalLazy, displayController, systemWindows,
-                imeController, handler, keyguardStateController, transactionPool);
+        // TODO(b/161116823): fetch DividerProxy from WM shell lib.
+        DividerController dividerController = new DividerController(context, displayController,
+                systemWindows, imeController, handler, transactionPool);
+        return new Divider(context, dividerController, keyguardStateController,
+                recentsOptionalLazy);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
index b6c6afd..6447c52 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
@@ -76,11 +76,14 @@
 public class DividerView extends FrameLayout implements OnTouchListener,
         OnComputeInternalInsetsListener {
     private static final String TAG = "DividerView";
-    private static final boolean DEBUG = Divider.DEBUG;
+    private static final boolean DEBUG = DividerController.DEBUG;
 
     public interface DividerCallbacks {
         void onDraggingStart();
         void onDraggingEnd();
+    }
+
+    interface RecentDrawnCallback {
         void growRecents();
     }
 
@@ -437,17 +440,17 @@
         releaseBackground();
     }
 
-    public void stopDragging(int position, SnapTarget target, long duration,
+    private void stopDragging(int position, SnapTarget target, long duration,
             Interpolator interpolator) {
         stopDragging(position, target, duration, 0 /* startDelay*/, 0 /* endDelay */, interpolator);
     }
 
-    public void stopDragging(int position, SnapTarget target, long duration,
+    private void stopDragging(int position, SnapTarget target, long duration,
             Interpolator interpolator, long endDelay) {
         stopDragging(position, target, duration, 0 /* startDelay*/, endDelay, interpolator);
     }
 
-    public void stopDragging(int position, SnapTarget target, long duration, long startDelay,
+    private void stopDragging(int position, SnapTarget target, long duration, long startDelay,
             long endDelay, Interpolator interpolator) {
         mHandle.setTouching(false, true /* animate */);
         flingTo(position, target, duration, startDelay, endDelay, interpolator);
@@ -1363,7 +1366,7 @@
                 null /* transaction */);
     }
 
-    void onRecentsDrawn() {
+    void onRecentsDrawn(RecentDrawnCallback callback) {
         updateDockSide();
         final int position = calculatePositionForInsetBounds();
         if (mState.animateAfterRecentsDrawn) {
@@ -1380,8 +1383,8 @@
         if (mState.growAfterRecentsDrawn) {
             mState.growAfterRecentsDrawn = false;
             updateDockSide();
-            if (mCallback != null) {
-                mCallback.growRecents();
+            if (callback != null) {
+                callback.growRecents();
             }
             stopDragging(position, getSnapAlgorithm().getMiddleTarget(), 336,
                     Interpolators.FAST_OUT_SLOW_IN);
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/ForcedResizableInfoActivityController.java b/packages/SystemUI/src/com/android/systemui/stackdivider/ForcedResizableInfoActivityController.java
index db7996e..f412cc0 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/ForcedResizableInfoActivityController.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/ForcedResizableInfoActivityController.java
@@ -75,7 +75,8 @@
         }
     }
 
-    public ForcedResizableInfoActivityController(Context context, Divider divider) {
+    public ForcedResizableInfoActivityController(Context context,
+            DividerController dividerController) {
         mContext = context;
         ActivityManagerWrapper.getInstance().registerTaskStackListener(
                 new TaskStackChangeListener() {
@@ -95,7 +96,7 @@
                         activityLaunchOnSecondaryDisplayFailed();
                     }
                 });
-        divider.registerInSplitScreenListener(mDockedStackExistsListener);
+        dividerController.registerInSplitScreenListener(mDockedStackExistsListener);
     }
 
     public void onAppTransitionFinished() {
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java
index 7a313dc..ef5e8a1 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java
@@ -35,7 +35,7 @@
 
 class SplitScreenTaskOrganizer extends TaskOrganizer {
     private static final String TAG = "SplitScreenTaskOrg";
-    private static final boolean DEBUG = Divider.DEBUG;
+    private static final boolean DEBUG = DividerController.DEBUG;
 
     RunningTaskInfo mPrimary;
     RunningTaskInfo mSecondary;
@@ -44,13 +44,13 @@
     SurfaceControl mPrimaryDim;
     SurfaceControl mSecondaryDim;
     Rect mHomeBounds = new Rect();
-    final Divider mDivider;
+    final DividerController mDividerController;
     private boolean mSplitScreenSupported = false;
 
     final SurfaceSession mSurfaceSession = new SurfaceSession();
 
-    SplitScreenTaskOrganizer(Divider divider) {
-        mDivider = divider;
+    SplitScreenTaskOrganizer(DividerController dividerController) {
+        mDividerController = dividerController;
     }
 
     void init() throws RemoteException {
@@ -75,11 +75,11 @@
     }
 
     SurfaceControl.Transaction getTransaction() {
-        return mDivider.mTransactionPool.acquire();
+        return mDividerController.mTransactionPool.acquire();
     }
 
     void releaseTransaction(SurfaceControl.Transaction t) {
-        mDivider.mTransactionPool.release(t);
+        mDividerController.mTransactionPool.release(t);
     }
 
     @Override
@@ -140,7 +140,7 @@
                 t.apply();
                 releaseTransaction(t);
 
-                mDivider.onTaskVanished();
+                mDividerController.onTaskVanished();
             }
         }
     }
@@ -150,7 +150,7 @@
         if (taskInfo.displayId != DEFAULT_DISPLAY) {
             return;
         }
-        mDivider.getHandler().post(() -> handleTaskInfoChanged(taskInfo));
+        mDividerController.post(() -> handleTaskInfoChanged(taskInfo));
     }
 
     /**
@@ -169,7 +169,7 @@
         }
         final boolean secondaryImpliedMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
                 || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS
-                        && mDivider.isHomeStackResizable());
+                        && mDividerController.isHomeStackResizable());
         final boolean primaryWasEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
         final boolean secondaryWasEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
         if (info.token.asBinder() == mPrimary.token.asBinder()) {
@@ -181,7 +181,7 @@
         final boolean secondaryIsEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
         final boolean secondaryImpliesMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
                 || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS
-                        && mDivider.isHomeStackResizable());
+                        && mDividerController.isHomeStackResizable());
         if (DEBUG) {
             Log.d(TAG, "onTaskInfoChanged " + mPrimary + "  " + mSecondary);
         }
@@ -197,14 +197,14 @@
                 Log.d(TAG, " at-least one split empty " + mPrimary.topActivityType
                         + "  " + mSecondary.topActivityType);
             }
-            if (mDivider.isDividerVisible()) {
+            if (mDividerController.isDividerVisible()) {
                 // 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);
                 }
-                mDivider.startDismissSplit();
+                mDividerController.startDismissSplit();
             } 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.
@@ -213,15 +213,15 @@
                 if (DEBUG) {
                     Log.d(TAG, "   was not in split, but primary is populated, so enter it");
                 }
-                mDivider.startEnterSplit();
+                mDividerController.startEnterSplit();
             }
         } else if (secondaryImpliesMinimize) {
             // Both splits are populated but the secondary split has a home/recents stack on top,
             // so enter minimized mode.
-            mDivider.ensureMinimizedSplit();
+            mDividerController.ensureMinimizedSplit();
         } else {
             // Both splits are populated by normal activities, so make sure we aren't minimized.
-            mDivider.ensureNormalSplit();
+            mDividerController.ensureNormalSplit();
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/SyncTransactionQueue.java b/packages/SystemUI/src/com/android/systemui/stackdivider/SyncTransactionQueue.java
index 6812f62..f2500e5 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/SyncTransactionQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/SyncTransactionQueue.java
@@ -33,7 +33,7 @@
  * Helper for serializing sync-transactions and corresponding callbacks.
  */
 class SyncTransactionQueue {
-    private static final boolean DEBUG = Divider.DEBUG;
+    private static final boolean DEBUG = DividerController.DEBUG;
     private static final String TAG = "SyncTransactionQueue";
 
     // Just a little longer than the sync-engine timeout of 5s
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
index 2b36812..82b10bd 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
@@ -50,7 +50,7 @@
 /**
  * Proxy to simplify calls into window manager/activity manager
  */
-public class WindowManagerProxy {
+class WindowManagerProxy {
 
     private static final String TAG = "WindowManagerProxy";
     private static final int[] HOME_AND_RECENTS = {ACTIVITY_TYPE_HOME, ACTIVITY_TYPE_RECENTS};
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
index 9560195..c1e8d03 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
@@ -404,7 +404,7 @@
             mContentDescriptionFormat = new SimpleDateFormat(format);
             /*
              * Search for an unquoted "a" in the format string, so we can
-             * add dummy characters around it to let us find it again after
+             * add marker characters around it to let us find it again after
              * formatting and change its size.
              */
             if (mAmPmStyle != AM_PM_STYLE_NORMAL) {
diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
index 103151d..7ee607c 100644
--- a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
+++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
@@ -219,7 +219,7 @@
         if (connected) {
             synchronized (mLock) {
                 if (mZombie) {
-                    // Sanity check - shouldn't happen
+                    // Validation check - shouldn't happen
                     if (mRemoteService == null) {
                         Slog.w(TAG, "Cannot resurrect sessions because remote service is null");
                         return;
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 43e3a04..d9fde0f 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -16,8 +16,6 @@
 
 package com.android.server.am;
 
-import static android.os.Process.THREAD_PRIORITY_FOREGROUND;
-
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_COMPACTION;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_FREEZER;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
@@ -256,7 +254,7 @@
             ProcessDependencies processDependencies) {
         mAm = am;
         mCachedAppOptimizerThread = new ServiceThread("CachedAppOptimizerThread",
-            THREAD_PRIORITY_FOREGROUND, true);
+            Process.THREAD_GROUP_SYSTEM, true);
         mProcStateThrottle = new HashSet<>();
         mProcessDependencies = processDependencies;
         mTestCallback = callback;
@@ -280,8 +278,6 @@
             updateProcStateThrottle();
             updateUseFreezer();
         }
-        Process.setThreadGroupAndCpuset(mCachedAppOptimizerThread.getThreadId(),
-                Process.THREAD_GROUP_SYSTEM);
     }
 
     /**
@@ -411,12 +407,15 @@
         mUseCompaction = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                     KEY_USE_COMPACTION, DEFAULT_USE_COMPACTION);
 
-        if (mUseCompaction) {
+        if (mUseCompaction && mCompactionHandler == null) {
             if (!mCachedAppOptimizerThread.isAlive()) {
                 mCachedAppOptimizerThread.start();
             }
 
             mCompactionHandler = new MemCompactionHandler();
+
+            Process.setThreadGroupAndCpuset(mCachedAppOptimizerThread.getThreadId(),
+                    Process.THREAD_GROUP_SYSTEM);
         }
     }
 
@@ -470,13 +469,16 @@
             mUseFreezer = isFreezerSupported();
         }
 
-        if (mUseFreezer) {
+        if (mUseFreezer && mFreezeHandler == null) {
             Slog.d(TAG_AM, "Freezer enabled");
             if (!mCachedAppOptimizerThread.isAlive()) {
                 mCachedAppOptimizerThread.start();
             }
 
             mFreezeHandler = new FreezeHandler();
+
+            Process.setThreadGroupAndCpuset(mCachedAppOptimizerThread.getThreadId(),
+                    Process.THREAD_GROUP_SYSTEM);
         }
     }
 
diff --git a/services/core/java/com/android/server/display/ColorFade.java b/services/core/java/com/android/server/display/ColorFade.java
index ec2b0c0..ad3cd67 100644
--- a/services/core/java/com/android/server/display/ColorFade.java
+++ b/services/core/java/com/android/server/display/ColorFade.java
@@ -491,7 +491,14 @@
 
                 mIsWideColor = SurfaceControl.getActiveColorMode(token)
                         == Display.COLOR_MODE_DISPLAY_P3;
-                SurfaceControl.screenshot(token, s);
+                SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer =
+                        mDisplayManagerInternal.systemScreenshot(mDisplayId);
+                s.attachAndQueueBufferWithColorSpace(screenshotBuffer.getHardwareBuffer(),
+                        screenshotBuffer.getColorSpace());
+
+                if (screenshotBuffer.containsSecureLayers()) {
+                    mTransaction.setSecure(mSurfaceControl, true).apply();
+                }
                 st.updateTexImage();
                 st.getTransformMatrix(mTexMatrix);
             } finally {
@@ -586,7 +593,6 @@
         }
 
         if (mSurfaceControl == null) {
-            Transaction t = new Transaction();
             try {
                 final SurfaceControl.Builder builder = new SurfaceControl.Builder(mSurfaceSession)
                         .setName("ColorFade")
@@ -602,15 +608,16 @@
                 return false;
             }
 
-            t.setLayerStack(mSurfaceControl, mDisplayLayerStack);
-            t.setWindowCrop(mSurfaceControl, mDisplayWidth, mDisplayHeight);
+            mTransaction.setLayerStack(mSurfaceControl, mDisplayLayerStack);
+            mTransaction.setWindowCrop(mSurfaceControl, mDisplayWidth, mDisplayHeight);
+            mSurfaceLayout = new NaturalSurfaceLayout(mDisplayManagerInternal,
+                    mDisplayId, mSurfaceControl);
+            mSurfaceLayout.onDisplayTransaction(mTransaction);
+            mTransaction.apply();
+
             mSurface = new Surface();
             mSurface.copyFrom(mSurfaceControl);
 
-            mSurfaceLayout = new NaturalSurfaceLayout(mDisplayManagerInternal,
-                    mDisplayId, mSurfaceControl);
-            mSurfaceLayout.onDisplayTransaction(t);
-            t.apply();
         }
         return true;
     }
@@ -652,7 +659,7 @@
         if (mSurfaceControl != null) {
             mSurfaceLayout.dispose();
             mSurfaceLayout = null;
-            new Transaction().remove(mSurfaceControl).apply();
+            mTransaction.remove(mSurfaceControl).apply();
             mSurface.release();
             mSurfaceControl = null;
             mSurfaceVisible = false;
diff --git a/services/core/java/com/android/server/os/TEST_MAPPING b/services/core/java/com/android/server/os/TEST_MAPPING
index a837fb4..d937af1 100644
--- a/services/core/java/com/android/server/os/TEST_MAPPING
+++ b/services/core/java/com/android/server/os/TEST_MAPPING
@@ -1,6 +1,14 @@
 {
   "presubmit": [
-    // TODO(159590499) add BugreportManagerTestCases
+    {
+      "file_patterns": ["Bugreport[^/]*\\.java"],
+      "name": "BugreportManagerTestCases",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        }
+      ]
+    },
     {
       "file_patterns": ["Bugreport[^/]*\\.java"],
       "name": "CtsBugreportTestCases",
diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
index 5415967..d48570f 100644
--- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java
+++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
@@ -337,6 +337,7 @@
     private int idleOptimizePackages(PackageManagerService pm, ArraySet<String> pkgs,
             long lowStorageThreshold) {
         ArraySet<String> updatedPackages = new ArraySet<>();
+        ArraySet<String> updatedPackagesDueToSecondaryDex = new ArraySet<>();
 
         try {
             final boolean supportSecondaryDex = supportSecondaryDex();
@@ -391,11 +392,14 @@
             }
 
             int secondaryResult = optimizePackages(pm, pkgs, lowStorageThreshold,
-                    /*isForPrimaryDex*/ false, updatedPackages);
+                    /*isForPrimaryDex*/ false, updatedPackagesDueToSecondaryDex);
             return secondaryResult;
         } finally {
             // Always let the pinner service know about changes.
             notifyPinService(updatedPackages);
+            // Only notify IORap the primary dex opt, because we don't want to
+            // invalidate traces unnecessary due to b/161633001 and that it's
+            // better to have a trace than no trace at all.
             notifyPackagesUpdated(updatedPackages);
         }
     }
diff --git a/services/core/java/com/android/server/storage/StorageUserConnection.java b/services/core/java/com/android/server/storage/StorageUserConnection.java
index 361a506..1c29c69 100644
--- a/services/core/java/com/android/server/storage/StorageUserConnection.java
+++ b/services/core/java/com/android/server/storage/StorageUserConnection.java
@@ -29,6 +29,7 @@
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.os.Bundle;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelableException;
@@ -64,9 +65,6 @@
     private static final String TAG = "StorageUserConnection";
 
     private static final int DEFAULT_REMOTE_TIMEOUT_SECONDS = 20;
-    // TODO(b/161702661): Workaround for demo user to have shorter timeout.
-    // This allows the DevicePolicyManagerService#enableSystemApp call to succeed without ANR.
-    private static final int DEMO_USER_REMOTE_TIMEOUT_SECONDS = 5;
 
     private final Object mLock = new Object();
     private final Context mContext;
@@ -75,6 +73,7 @@
     private final ActiveConnection mActiveConnection = new ActiveConnection();
     private final boolean mIsDemoUser;
     @GuardedBy("mLock") private final Map<String, Session> mSessions = new HashMap<>();
+    @GuardedBy("mLock") @Nullable private HandlerThread mHandlerThread;
 
     public StorageUserConnection(Context context, int userId, StorageSessionController controller) {
         mContext = Objects.requireNonNull(context);
@@ -82,6 +81,10 @@
         mSessionController = controller;
         mIsDemoUser = LocalServices.getService(UserManagerInternal.class)
                 .getUserInfo(userId).isDemo();
+        if (mIsDemoUser) {
+            mHandlerThread = new HandlerThread("StorageUserConnectionThread-" + mUserId);
+            mHandlerThread.start();
+        }
     }
 
     /**
@@ -188,6 +191,9 @@
      */
     public void close() {
         mActiveConnection.close();
+        if (mIsDemoUser) {
+            mHandlerThread.quit();
+        }
     }
 
     /** Returns all created sessions. */
@@ -207,8 +213,7 @@
 
     private void waitForLatch(CountDownLatch latch, String reason) throws TimeoutException {
         try {
-            if (!latch.await(mIsDemoUser ? DEMO_USER_REMOTE_TIMEOUT_SECONDS
-                            : DEFAULT_REMOTE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+            if (!latch.await(DEFAULT_REMOTE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
                 // TODO(b/140025078): Call ActivityManager ANR API?
                 Slog.wtf(TAG, "Failed to bind to the ExternalStorageService for user " + mUserId);
                 throw new TimeoutException("Latch wait for " + reason + " elapsed");
@@ -424,15 +429,32 @@
                 };
 
                 Slog.i(TAG, "Binding to the ExternalStorageService for user " + mUserId);
-                if (mContext.bindServiceAsUser(new Intent().setComponent(name), mServiceConnection,
-                        Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
-                        UserHandle.of(mUserId))) {
-                    Slog.i(TAG, "Bound to the ExternalStorageService for user " + mUserId);
-                    return mLatch;
+                if (mIsDemoUser) {
+                    // Schedule on a worker thread for demo user to avoid deadlock
+                    if (mContext.bindServiceAsUser(new Intent().setComponent(name),
+                                    mServiceConnection,
+                                    Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
+                                    mHandlerThread.getThreadHandler(),
+                                    UserHandle.of(mUserId))) {
+                        Slog.i(TAG, "Bound to the ExternalStorageService for user " + mUserId);
+                        return mLatch;
+                    } else {
+                        mIsConnecting = false;
+                        throw new ExternalStorageServiceException(
+                                "Failed to bind to the ExternalStorageService for user " + mUserId);
+                    }
                 } else {
-                    mIsConnecting = false;
-                    throw new ExternalStorageServiceException(
-                            "Failed to bind to the ExternalStorageService for user " + mUserId);
+                    if (mContext.bindServiceAsUser(new Intent().setComponent(name),
+                                    mServiceConnection,
+                                    Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
+                                    UserHandle.of(mUserId))) {
+                        Slog.i(TAG, "Bound to the ExternalStorageService for user " + mUserId);
+                        return mLatch;
+                    } else {
+                        mIsConnecting = false;
+                        throw new ExternalStorageServiceException(
+                                "Failed to bind to the ExternalStorageService for user " + mUserId);
+                    }
                 }
             }
         }