Add GlobalActionsColumnLayout to replace HardwareUILayout.

Refactor code to improve code re-use and enable testing, and add lots of unit tests.
HardwareUILayout is no longer used.

Test: Automated tests pass. Manual testing with different display sizes and with panel plugin enabled/disabled.

Fixes: 130808177
Fixes: 128372852

Change-Id: I1e48d226973a9b610cece2691af7b233cdb5235c
diff --git a/packages/SystemUI/res/layout-land/global_actions_column.xml b/packages/SystemUI/res/layout-land/global_actions_column.xml
new file mode 100644
index 0000000..99a4e13
--- /dev/null
+++ b/packages/SystemUI/res/layout-land/global_actions_column.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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
+  -->
+
+<com.android.systemui.globalactions.GlobalActionsColumnLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@id/global_actions_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal"
+    android:clipToPadding="false"
+    android:theme="@style/qs_theme"
+    android:gravity="center_horizontal | top"
+    android:clipChildren="false"
+>
+    <LinearLayout
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:padding="0dp"
+        android:orientation="horizontal"
+        android:clipChildren="false"
+        android:clipToPadding="false"
+    >
+        <!-- Grid of action items -->
+        <LinearLayout
+            android:id="@android:id/list"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_marginTop="@dimen/global_actions_grid_side_margin"
+            android:translationZ="@dimen/global_actions_translate"
+            android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingTop="@dimen/global_actions_grid_vertical_padding"
+            android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+            android:background="?android:attr/colorBackgroundFloating"
+        />
+        <!-- For separated items-->
+        <LinearLayout
+            android:id="@+id/separated_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="@dimen/global_actions_grid_side_margin"
+            android:layout_marginTop="@dimen/global_actions_grid_side_margin"
+            android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingTop="@dimen/global_actions_grid_vertical_padding"
+            android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+            android:orientation="horizontal"
+            android:background="?android:attr/colorBackgroundFloating"
+            android:translationZ="@dimen/global_actions_translate"
+        />
+    </LinearLayout>
+
+</com.android.systemui.globalactions.GlobalActionsColumnLayout>
diff --git a/packages/SystemUI/res/layout-land/global_actions_column_seascape.xml b/packages/SystemUI/res/layout-land/global_actions_column_seascape.xml
new file mode 100644
index 0000000..0f86131
--- /dev/null
+++ b/packages/SystemUI/res/layout-land/global_actions_column_seascape.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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
+  -->
+
+<com.android.systemui.globalactions.GlobalActionsColumnLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@id/global_actions_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal"
+    android:clipToPadding="false"
+    android:theme="@style/qs_theme"
+    android:gravity="center_horizontal | bottom"
+    android:clipChildren="false"
+>
+    <LinearLayout
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:padding="0dp"
+        android:orientation="horizontal"
+    >
+        <!-- Grid of action items -->
+        <com.android.systemui.globalactions.ListGridLayout
+            android:id="@android:id/list"
+            android:layout_gravity="bottom|left"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_marginBottom="@dimen/global_actions_grid_side_margin"
+            android:translationZ="@dimen/global_actions_translate"
+            android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingTop="@dimen/global_actions_grid_vertical_padding"
+            android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+            android:background="?android:attr/colorBackgroundFloating"
+        />
+        <!-- For separated items-->
+        <LinearLayout
+            android:id="@+id/separated_button"
+            android:layout_gravity="top|left"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="@dimen/global_actions_grid_side_margin"
+            android:layout_marginBottom="@dimen/global_actions_grid_side_margin"
+            android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingTop="@dimen/global_actions_grid_vertical_padding"
+            android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+            android:orientation="horizontal"
+            android:background="?android:attr/colorBackgroundFloating"
+            android:translationZ="@dimen/global_actions_translate"
+        />
+    </LinearLayout>
+
+</com.android.systemui.globalactions.GlobalActionsColumnLayout>
diff --git a/packages/SystemUI/res/layout/global_actions_column.xml b/packages/SystemUI/res/layout/global_actions_column.xml
new file mode 100644
index 0000000..b58146b
--- /dev/null
+++ b/packages/SystemUI/res/layout/global_actions_column.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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
+  -->
+
+<com.android.systemui.globalactions.GlobalActionsColumnLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@id/global_actions_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:clipToPadding="false"
+    android:theme="@style/qs_theme"
+    android:gravity="center_vertical | right"
+    android:clipChildren="false"
+>
+    <LinearLayout
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:gravity="top | right"
+        android:orientation="vertical"
+        android:padding="0dp"
+        android:layout_marginTop="@dimen/global_actions_grid_container_bottom_margin"
+        android:layout_marginBottom="@dimen/global_actions_grid_container_bottom_margin"
+    >
+        <!-- Global actions is right-aligned to be physically near power button -->
+        <LinearLayout
+            android:id="@android:id/list"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:gravity="right"
+            android:translationZ="@dimen/global_actions_translate"
+            android:layout_marginRight="@dimen/global_actions_grid_side_margin"
+            android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingTop="@dimen/global_actions_grid_vertical_padding"
+            android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+        />
+
+        <!-- For separated items-->
+        <LinearLayout
+            android:id="@+id/separated_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/global_actions_grid_side_margin"
+            android:layout_marginRight="@dimen/global_actions_grid_side_margin"
+            android:paddingLeft="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingRight="@dimen/global_actions_grid_horizontal_padding"
+            android:paddingTop="@dimen/global_actions_grid_vertical_padding"
+            android:paddingBottom="@dimen/global_actions_grid_vertical_padding"
+            android:orientation="vertical"
+            android:gravity="center"
+            android:translationZ="@dimen/global_actions_translate"
+        />
+    </LinearLayout>
+
+</com.android.systemui.globalactions.GlobalActionsColumnLayout>
diff --git a/packages/SystemUI/src/com/android/systemui/HardwareUiLayout.java b/packages/SystemUI/src/com/android/systemui/HardwareUiLayout.java
index 802903d..ad2e002 100644
--- a/packages/SystemUI/src/com/android/systemui/HardwareUiLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/HardwareUiLayout.java
@@ -80,16 +80,6 @@
     }
 
     @Override
-    public void removeAllItems() {
-        if (mList != null) {
-            mList.removeAllViews();
-        }
-        if (mSeparatedView != null) {
-            mSeparatedView.removeAllViews();
-        }
-    }
-
-    @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         updateSettings();
diff --git a/packages/SystemUI/src/com/android/systemui/MultiListLayout.java b/packages/SystemUI/src/com/android/systemui/MultiListLayout.java
index f8287a4..d153fb0 100644
--- a/packages/SystemUI/src/com/android/systemui/MultiListLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/MultiListLayout.java
@@ -45,11 +45,6 @@
     protected abstract ViewGroup getListView();
 
     /**
-     * Removes all child items from the separated and list views, if they exist.
-     */
-    protected abstract void removeAllItems();
-
-    /**
      * Sets the divided view, which may have a differently-colored background.
      */
     public abstract void setDivisionView(View v);
@@ -110,6 +105,25 @@
         onUpdateList();
     }
 
+    protected void removeAllSeparatedViews() {
+        ViewGroup separated = getSeparatedView();
+        if (separated != null) {
+            separated.removeAllViews();
+        }
+    }
+
+    protected void removeAllListViews() {
+        ViewGroup list = getListView();
+        if (list != null) {
+            list.removeAllViews();
+        }
+    }
+
+    protected void removeAllItems() {
+        removeAllListViews();
+        removeAllSeparatedViews();
+    }
+
     protected void onUpdateList() {
         removeAllItems();
         setSeparatedViewVisibility(mAdapter.hasSeparatedItems());
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsColumnLayout.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsColumnLayout.java
new file mode 100644
index 0000000..5907028
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsColumnLayout.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2019 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.globalactions;
+
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.R;
+
+/**
+ * Grid-based implementation of the button layout created by the global actions dialog.
+ */
+public class GlobalActionsColumnLayout extends GlobalActionsLayout {
+    private boolean mLastSnap;
+
+    public GlobalActionsColumnLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+
+        post(() -> updateSnap());
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @VisibleForTesting
+    protected boolean shouldReverseListItems() {
+        int rotation = getCurrentRotation();
+        if (rotation == ROTATION_NONE) {
+            return false;
+        }
+        if (getCurrentLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+            return rotation == ROTATION_LANDSCAPE;
+        }
+        return rotation == ROTATION_SEASCAPE;
+    }
+
+    @Override
+    public void onUpdateList() {
+        super.onUpdateList();
+        updateChildOrdering();
+    }
+
+    private void updateChildOrdering() {
+        if (shouldReverseListItems()) {
+            getListView().bringToFront();
+        } else {
+            getSeparatedView().bringToFront();
+        }
+    }
+
+    /**
+     *  Snap this layout to align with the power button.
+     */
+    @VisibleForTesting
+    protected void snapToPowerButton() {
+        int offset = getPowerButtonOffsetDistance();
+        switch (getCurrentRotation()) {
+            case (ROTATION_LANDSCAPE):
+                setPadding(offset, 0, 0, 0);
+                setGravity(Gravity.LEFT | Gravity.TOP);
+                break;
+            case (ROTATION_SEASCAPE):
+                setPadding(0, 0, offset, 0);
+                setGravity(Gravity.RIGHT | Gravity.BOTTOM);
+                break;
+            default:
+                setPadding(0, offset, 0, 0);
+                setGravity(Gravity.TOP | Gravity.RIGHT);
+                break;
+        }
+    }
+
+    /**
+     *  Detach this layout from snapping to the power button and instead center along that edge.
+     */
+    @VisibleForTesting
+    protected void centerAlongEdge() {
+        switch (getCurrentRotation()) {
+            case (ROTATION_LANDSCAPE):
+                setPadding(0, 0, 0, 0);
+                setGravity(Gravity.CENTER_HORIZONTAL | Gravity.TOP);
+                break;
+            case (ROTATION_SEASCAPE):
+                setPadding(0, 0, 0, 0);
+                setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
+                break;
+            default:
+                setPadding(0, 0, 0, 0);
+                setGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+                break;
+        }
+    }
+
+    /**
+     * Determines the distance from the top of the screen to the power button.
+     */
+    @VisibleForTesting
+    protected int getPowerButtonOffsetDistance() {
+        return Math.round(getContext().getResources().getDimension(
+                R.dimen.global_actions_top_padding));
+    }
+
+    /**
+     * Check whether there is enough extra space below the dialog such that we can offset the top
+     * of the dialog from the top of the phone to line it up with the power button, then either
+     * snap the dialog to the power button or center it along the edge with snapToPowerButton.
+     */
+    @VisibleForTesting
+    protected boolean shouldSnapToPowerButton() {
+        int offsetSize = getPowerButtonOffsetDistance();
+        int dialogSize;
+        int screenSize;
+        View wrapper = getWrapper();
+        int rotation = getCurrentRotation();
+        if (rotation == ROTATION_NONE) {
+            dialogSize = wrapper.getMeasuredHeight();
+            screenSize = getMeasuredHeight();
+        } else {
+            dialogSize = wrapper.getMeasuredWidth();
+            screenSize = getMeasuredWidth();
+        }
+        return dialogSize + offsetSize < screenSize;
+    }
+
+    @VisibleForTesting
+    protected void updateSnap() {
+        boolean snap = shouldSnapToPowerButton();
+        if (snap != mLastSnap) {
+            if (snap) {
+                snapToPowerButton();
+            } else {
+                centerAlongEdge();
+            }
+        }
+        mLastSnap = snap;
+    }
+
+    @VisibleForTesting
+    protected float getGridItemSize() {
+        return getContext().getResources().getDimension(R.dimen.global_actions_grid_item_height);
+    }
+
+    @VisibleForTesting
+    protected float getAnimationDistance() {
+        return getGridItemSize() / 2;
+    }
+
+    @Override
+    public float getAnimationOffsetX() {
+        if (getCurrentRotation() == ROTATION_NONE) {
+            return getAnimationDistance();
+        }
+        return 0;
+    }
+
+    @Override
+    public float getAnimationOffsetY() {
+        switch (getCurrentRotation()) {
+            case ROTATION_LANDSCAPE:
+                return -getAnimationDistance();
+            case ROTATION_SEASCAPE:
+                return getAnimationDistance();
+            default: // Portrait
+                return 0;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index 9b69dc5..2b006ce 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -1582,13 +1582,20 @@
         }
 
         private int getGlobalActionsLayoutId(Context context) {
-            if (isForceGridEnabled(context) || shouldUsePanel()) {
-                if (RotationUtils.getRotation(context) == RotationUtils.ROTATION_SEASCAPE) {
+            boolean useGridLayout = isForceGridEnabled(context) || shouldUsePanel();
+            if (RotationUtils.getRotation(context) == RotationUtils.ROTATION_SEASCAPE) {
+                if (useGridLayout) {
                     return com.android.systemui.R.layout.global_actions_grid_seascape;
+                } else {
+                    return com.android.systemui.R.layout.global_actions_column_seascape;
                 }
-                return com.android.systemui.R.layout.global_actions_grid;
+            } else {
+                if (useGridLayout) {
+                    return com.android.systemui.R.layout.global_actions_grid;
+                } else {
+                    return com.android.systemui.R.layout.global_actions_column;
+                }
             }
-            return com.android.systemui.R.layout.global_actions_wrapped;
         }
 
         @Override
@@ -1711,7 +1718,7 @@
         }
 
         public void onRotate(int from, int to) {
-            if (mShowing && (shouldUsePanel() || isForceGridEnabled(mContext))) {
+            if (mShowing) {
                 refreshDialog();
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsGridLayout.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsGridLayout.java
index 554ed73..e1462d1 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsGridLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsGridLayout.java
@@ -22,58 +22,23 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.HardwareBgDrawable;
-import com.android.systemui.MultiListLayout;
-import com.android.systemui.util.leak.RotationUtils;
 
 /**
  * Grid-based implementation of the button layout created by the global actions dialog.
  */
-public class GlobalActionsGridLayout extends MultiListLayout {
-
-    boolean mBackgroundsSet;
-
+public class GlobalActionsGridLayout extends GlobalActionsLayout {
     public GlobalActionsGridLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
-    private void setBackgrounds() {
-        int gridBackgroundColor = getResources().getColor(
-                com.android.systemui.R.color.global_actions_grid_background, null);
-        int separatedBackgroundColor = getResources().getColor(
-                com.android.systemui.R.color.global_actions_separated_background, null);
-        HardwareBgDrawable listBackground  = new HardwareBgDrawable(true, true, getContext());
-        HardwareBgDrawable separatedBackground = new HardwareBgDrawable(true, true, getContext());
-        listBackground.setTint(gridBackgroundColor);
-        separatedBackground.setTint(separatedBackgroundColor);
-        getListView().setBackground(listBackground);
-        getSeparatedView().setBackground(separatedBackground);
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
-        // backgrounds set only once, the first time onMeasure is called after inflation
-        if (getListView() != null && !mBackgroundsSet) {
-            setBackgrounds();
-            mBackgroundsSet = true;
-        }
-    }
-
     @VisibleForTesting
-    protected int getCurrentRotation() {
-        return RotationUtils.getRotation(mContext);
-    }
-
-    @VisibleForTesting
-    protected void setupListView(ListGridLayout listView, int itemCount) {
-        listView.setExpectedCount(itemCount);
+    protected void setupListView() {
+        ListGridLayout listView = getListView();
+        listView.setExpectedCount(mAdapter.countListItems());
         listView.setReverseSublists(shouldReverseSublists());
         listView.setReverseItems(shouldReverseListItems());
         listView.setSwapRowsAndColumns(shouldSwapRowsAndColumns());
@@ -81,29 +46,8 @@
 
     @Override
     public void onUpdateList() {
+        setupListView();
         super.onUpdateList();
-
-        ViewGroup separatedView = getSeparatedView();
-        ListGridLayout listView = getListView();
-        setupListView(listView, mAdapter.countListItems());
-
-        for (int i = 0; i < mAdapter.getCount(); i++) {
-            // generate the view item
-            View v;
-            boolean separated = mAdapter.shouldBeSeparated(i);
-            if (separated) {
-                v = mAdapter.getView(i, null, separatedView);
-            } else {
-                v = mAdapter.getView(i, null, listView);
-            }
-            Log.d("GlobalActionsGridLayout", "View: " + v);
-
-            if (separated) {
-                separatedView.addView(v);
-            } else {
-                listView.addItem(v);
-            }
-        }
         updateSeparatedItemSize();
     }
 
@@ -111,7 +55,8 @@
      * If the separated view contains only one item, expand the bounds of that item to take up the
      * entire view, so that the whole thing is touch-able.
      */
-    private void updateSeparatedItemSize() {
+    @VisibleForTesting
+    protected void updateSeparatedItemSize() {
         ViewGroup separated = getSeparatedView();
         if (separated.getChildCount() == 0) {
             return;
@@ -129,13 +74,24 @@
     }
 
     @Override
-    protected ViewGroup getSeparatedView() {
-        return findViewById(com.android.systemui.R.id.separated_button);
+    protected ListGridLayout getListView() {
+        return (ListGridLayout) super.getListView();
     }
 
     @Override
-    protected ListGridLayout getListView() {
-        return findViewById(android.R.id.list);
+    protected void removeAllListViews() {
+        ListGridLayout list = getListView();
+        if (list != null) {
+            list.removeAllItems();
+        }
+    }
+
+    @Override
+    protected void addToListView(View v, boolean reverse) {
+        ListGridLayout list = getListView();
+        if (list != null) {
+            list.addItem(v);
+        }
     }
 
     @Override
@@ -174,12 +130,7 @@
         return true;
     }
 
-    /**
-     * Determines whether the ListGridLayout should reverse the ordering of items within sublists.
-     * Used for RTL languages to ensure that items appear in the same positions, without having to
-     * override layoutDirection, which breaks Talkback ordering.
-     */
-    @VisibleForTesting
+    @Override
     protected boolean shouldReverseListItems() {
         int rotation = getCurrentRotation();
         boolean reverse = false; // should we add items to parents in the reverse order?
@@ -187,20 +138,13 @@
                 || rotation == ROTATION_SEASCAPE) {
             reverse = !reverse; // if we're in portrait or seascape, reverse items
         }
-        if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+        if (getCurrentLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
             reverse = !reverse; // if we're in an RTL language, reverse items (again)
         }
         return reverse;
     }
 
-    /**
-     * Not ued in this implementation of the Global Actions Menu, but necessary for some others.
-     */
-    @Override
-    public void setDivisionView(View v) {
-        // do nothing
-    }
-
+    @VisibleForTesting
     protected float getAnimationDistance() {
         int rows = getListView().getRowCount();
         float gridItemSize = getContext().getResources().getDimension(
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsLayout.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsLayout.java
new file mode 100644
index 0000000..f755a93
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsLayout.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2019 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.globalactions;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.HardwareBgDrawable;
+import com.android.systemui.MultiListLayout;
+import com.android.systemui.R;
+import com.android.systemui.util.leak.RotationUtils;
+
+import java.util.Locale;
+
+/**
+ * Grid-based implementation of the button layout created by the global actions dialog.
+ */
+public abstract class GlobalActionsLayout extends MultiListLayout {
+
+    boolean mBackgroundsSet;
+
+    public GlobalActionsLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    private void setBackgrounds() {
+        int gridBackgroundColor = getResources().getColor(
+                R.color.global_actions_grid_background, null);
+        int separatedBackgroundColor = getResources().getColor(
+                R.color.global_actions_separated_background, null);
+        HardwareBgDrawable listBackground  = new HardwareBgDrawable(true, true, getContext());
+        HardwareBgDrawable separatedBackground = new HardwareBgDrawable(true, true, getContext());
+        listBackground.setTint(gridBackgroundColor);
+        separatedBackground.setTint(separatedBackgroundColor);
+        getListView().setBackground(listBackground);
+        getSeparatedView().setBackground(separatedBackground);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        // backgrounds set only once, the first time onMeasure is called after inflation
+        if (getListView() != null && !mBackgroundsSet) {
+            setBackgrounds();
+            mBackgroundsSet = true;
+        }
+    }
+
+    protected void addToListView(View v, boolean reverse) {
+        if (reverse) {
+            getListView().addView(v, 0);
+        } else {
+            getListView().addView(v);
+        }
+    }
+
+    protected void addToSeparatedView(View v, boolean reverse) {
+        if (reverse) {
+            getSeparatedView().addView(v, 0);
+        } else {
+            getSeparatedView().addView(v);
+        }
+    }
+
+    @VisibleForTesting
+    protected int getCurrentLayoutDirection() {
+        return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault());
+    }
+
+    @VisibleForTesting
+    protected int getCurrentRotation() {
+        return RotationUtils.getRotation(mContext);
+    }
+
+    /**
+     * Determines whether the ListGridLayout should reverse the ordering of items within sublists.
+     * Used for RTL languages to ensure that items appear in the same positions, without having to
+     * override layoutDirection, which breaks Talkback ordering.
+     */
+    protected abstract boolean shouldReverseListItems();
+
+    @Override
+    public void onUpdateList() {
+        super.onUpdateList();
+
+        ViewGroup separatedView = getSeparatedView();
+        ViewGroup listView = getListView();
+
+        for (int i = 0; i < mAdapter.getCount(); i++) {
+            // generate the view item
+            View v;
+            boolean separated = mAdapter.shouldBeSeparated(i);
+            if (separated) {
+                v = mAdapter.getView(i, null, separatedView);
+            } else {
+                v = mAdapter.getView(i, null, listView);
+            }
+            if (separated) {
+                addToSeparatedView(v, false);
+            } else {
+                addToListView(v, shouldReverseListItems());
+            }
+        }
+    }
+
+    @Override
+    protected ViewGroup getSeparatedView() {
+        return findViewById(R.id.separated_button);
+    }
+
+    @Override
+    protected ViewGroup getListView() {
+        return findViewById(android.R.id.list);
+    }
+
+    protected View getWrapper() {
+        return getChildAt(0);
+    }
+
+    /**
+     * Not used in this implementation of the Global Actions Menu, but necessary for some others.
+     */
+    @Override
+    public void setDivisionView(View v) {
+        // do nothing
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsColumnLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsColumnLayoutTest.java
new file mode 100644
index 0000000..16d665c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsColumnLayoutTest.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2019 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.globalactions;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.util.leak.RotationUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link ListGridLayout}.
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class GlobalActionsColumnLayoutTest extends SysuiTestCase {
+
+    private GlobalActionsColumnLayout mColumnLayout;
+
+    @Before
+    public void setUp() throws Exception {
+        mColumnLayout = spy((GlobalActionsColumnLayout)
+                LayoutInflater.from(mContext).inflate(R.layout.global_actions_column, null));
+    }
+
+    @Test
+    public void testShouldReverseListItems() {
+        doReturn(View.LAYOUT_DIRECTION_LTR).when(mColumnLayout).getCurrentLayoutDirection();
+
+        doReturn(RotationUtils.ROTATION_LANDSCAPE).when(mColumnLayout).getCurrentRotation();
+        assertEquals(false, mColumnLayout.shouldReverseListItems());
+
+        doReturn(RotationUtils.ROTATION_NONE).when(mColumnLayout).getCurrentRotation();
+        assertEquals(false, mColumnLayout.shouldReverseListItems());
+
+        doReturn(RotationUtils.ROTATION_SEASCAPE).when(mColumnLayout).getCurrentRotation();
+        assertEquals(true, mColumnLayout.shouldReverseListItems());
+
+        doReturn(View.LAYOUT_DIRECTION_RTL).when(mColumnLayout).getCurrentLayoutDirection();
+
+        doReturn(RotationUtils.ROTATION_LANDSCAPE).when(mColumnLayout).getCurrentRotation();
+        assertEquals(true, mColumnLayout.shouldReverseListItems());
+
+        doReturn(RotationUtils.ROTATION_NONE).when(mColumnLayout).getCurrentRotation();
+        assertEquals(false, mColumnLayout.shouldReverseListItems());
+
+        doReturn(RotationUtils.ROTATION_SEASCAPE).when(mColumnLayout).getCurrentRotation();
+        assertEquals(false, mColumnLayout.shouldReverseListItems());
+    }
+
+    @Test
+    public void testGetAnimationOffsetX() {
+        doReturn(50f).when(mColumnLayout).getAnimationDistance();
+
+        doReturn(RotationUtils.ROTATION_NONE).when(mColumnLayout).getCurrentRotation();
+        assertEquals(50f, mColumnLayout.getAnimationOffsetX(), .01);
+
+        doReturn(RotationUtils.ROTATION_LANDSCAPE).when(mColumnLayout).getCurrentRotation();
+        assertEquals(0, mColumnLayout.getAnimationOffsetX(), .01);
+
+        doReturn(RotationUtils.ROTATION_SEASCAPE).when(mColumnLayout).getCurrentRotation();
+        assertEquals(0, mColumnLayout.getAnimationOffsetX(), .01);
+    }
+
+    @Test
+    public void testGetAnimationOffsetY() {
+        doReturn(50f).when(mColumnLayout).getAnimationDistance();
+
+        doReturn(RotationUtils.ROTATION_NONE).when(mColumnLayout).getCurrentRotation();
+        assertEquals(0, mColumnLayout.getAnimationOffsetY(), .01);
+
+        doReturn(RotationUtils.ROTATION_LANDSCAPE).when(mColumnLayout).getCurrentRotation();
+        assertEquals(-50f, mColumnLayout.getAnimationOffsetY(), .01);
+
+        doReturn(RotationUtils.ROTATION_SEASCAPE).when(mColumnLayout).getCurrentRotation();
+        assertEquals(50f, mColumnLayout.getAnimationOffsetY(), .01);
+    }
+
+    @Test
+    public void testSnapToPowerButton_portrait() {
+        doReturn(RotationUtils.ROTATION_NONE).when(mColumnLayout).getCurrentRotation();
+        doReturn(50).when(mColumnLayout).getPowerButtonOffsetDistance();
+
+        mColumnLayout.snapToPowerButton();
+        assertEquals(Gravity.TOP | Gravity.RIGHT, mColumnLayout.getGravity());
+        assertEquals(50, mColumnLayout.getPaddingTop(), .01);
+    }
+
+    @Test
+    public void testCenterAlongEdge_portrait() {
+        doReturn(RotationUtils.ROTATION_NONE).when(mColumnLayout).getCurrentRotation();
+
+        mColumnLayout.centerAlongEdge();
+        assertEquals(Gravity.CENTER_VERTICAL | Gravity.RIGHT, mColumnLayout.getGravity());
+        assertEquals(0, mColumnLayout.getPaddingTop(), .01);
+    }
+
+    @Test
+    public void testUpdateSnap_initialState() {
+        doReturn(false).when(mColumnLayout).shouldSnapToPowerButton();
+
+        mColumnLayout.updateSnap(); // should do nothing, since snap has not changed from init state
+
+        verify(mColumnLayout, times(0)).snapToPowerButton();
+        verify(mColumnLayout, times(0)).centerAlongEdge();
+    }
+
+    @Test
+    public void testUpdateSnap_snapThenSnap() {
+        doReturn(true).when(mColumnLayout).shouldSnapToPowerButton();
+
+        mColumnLayout.updateSnap(); // should snap to power button
+
+        verify(mColumnLayout, times(1)).snapToPowerButton();
+        verify(mColumnLayout, times(0)).centerAlongEdge();
+
+        mColumnLayout.updateSnap(); // should do nothing, since this is the same state as last time
+
+        verify(mColumnLayout, times(1)).snapToPowerButton();
+        verify(mColumnLayout, times(0)).centerAlongEdge();
+    }
+
+    @Test
+    public void testUpdateSnap_snapThenCenter() {
+        doReturn(true).when(mColumnLayout).shouldSnapToPowerButton();
+
+        mColumnLayout.updateSnap(); // should snap to power button
+
+        verify(mColumnLayout, times(1)).snapToPowerButton();
+        verify(mColumnLayout, times(0)).centerAlongEdge();
+
+        doReturn(false).when(mColumnLayout).shouldSnapToPowerButton();
+
+        mColumnLayout.updateSnap(); // should center to edge
+
+        verify(mColumnLayout, times(1)).snapToPowerButton();
+        verify(mColumnLayout, times(1)).centerAlongEdge();
+    }
+
+    @Test
+    public void testShouldSnapToPowerButton_vertical() {
+        doReturn(RotationUtils.ROTATION_NONE).when(mColumnLayout).getCurrentRotation();
+        doReturn(300).when(mColumnLayout).getPowerButtonOffsetDistance();
+        doReturn(1000).when(mColumnLayout).getMeasuredHeight();
+        View wrapper = spy(new View(mContext, null));
+        doReturn(wrapper).when(mColumnLayout).getWrapper();
+        doReturn(500).when(wrapper).getMeasuredHeight();
+
+        assertEquals(true, mColumnLayout.shouldSnapToPowerButton());
+
+        doReturn(600).when(mColumnLayout).getMeasuredHeight();
+
+        assertEquals(false, mColumnLayout.shouldSnapToPowerButton());
+    }
+
+    @Test
+    public void testShouldSnapToPowerButton_horizontal() {
+        doReturn(RotationUtils.ROTATION_LANDSCAPE).when(mColumnLayout).getCurrentRotation();
+        doReturn(300).when(mColumnLayout).getPowerButtonOffsetDistance();
+        doReturn(1000).when(mColumnLayout).getMeasuredWidth();
+        View wrapper = spy(new View(mContext, null));
+        doReturn(wrapper).when(mColumnLayout).getWrapper();
+        doReturn(500).when(wrapper).getMeasuredWidth();
+
+        assertEquals(true, mColumnLayout.shouldSnapToPowerButton());
+
+        doReturn(600).when(mColumnLayout).getMeasuredWidth();
+
+        assertEquals(false, mColumnLayout.shouldSnapToPowerButton());
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java
index 3c52e9d..a396f3e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java
@@ -18,12 +18,8 @@
 
 import static junit.framework.Assert.assertEquals;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
 
 import android.testing.AndroidTestingRunner;
 import android.view.LayoutInflater;
@@ -32,7 +28,6 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.systemui.MultiListLayout;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.util.leak.RotationUtils;
@@ -49,61 +44,12 @@
 public class GlobalActionsGridLayoutTest extends SysuiTestCase {
 
     private GlobalActionsGridLayout mGridLayout;
-    private TestAdapter mAdapter;
     private ListGridLayout mListGrid;
 
-    private class TestAdapter extends MultiListLayout.MultiListAdapter {
-        @Override
-        public void onClickItem(int index) { }
-
-        @Override
-        public boolean onLongClickItem(int index) {
-            return true;
-        }
-
-        @Override
-        public int countSeparatedItems() {
-            return -1;
-        }
-
-        @Override
-        public int countListItems() {
-            return -1;
-        }
-
-        @Override
-        public boolean shouldBeSeparated(int position) {
-            return false;
-        }
-
-        @Override
-        public int getCount() {
-            return countSeparatedItems() + countListItems();
-        }
-
-        @Override
-        public Object getItem(int position) {
-            return null;
-        }
-
-        @Override
-        public long getItemId(int position) {
-            return -1;
-        }
-
-        @Override
-        public View getView(int position, View convertView, ViewGroup parent) {
-            return null;
-        }
-    }
-
-
     @Before
     public void setUp() throws Exception {
         mGridLayout = spy((GlobalActionsGridLayout)
                 LayoutInflater.from(mContext).inflate(R.layout.global_actions_grid, null));
-        mAdapter = spy(new TestAdapter());
-        mGridLayout.setAdapter(mAdapter);
         mListGrid = spy(mGridLayout.getListView());
         doReturn(mListGrid).when(mGridLayout).getListView();
     }
@@ -122,7 +68,7 @@
 
     @Test
     public void testShouldReverseListItems() {
-        doReturn(View.LAYOUT_DIRECTION_LTR).when(mGridLayout).getLayoutDirection();
+        doReturn(View.LAYOUT_DIRECTION_LTR).when(mGridLayout).getCurrentLayoutDirection();
 
         doReturn(RotationUtils.ROTATION_LANDSCAPE).when(mGridLayout).getCurrentRotation();
         assertEquals(false, mGridLayout.shouldReverseListItems());
@@ -133,7 +79,7 @@
         doReturn(RotationUtils.ROTATION_SEASCAPE).when(mGridLayout).getCurrentRotation();
         assertEquals(true, mGridLayout.shouldReverseListItems());
 
-        doReturn(View.LAYOUT_DIRECTION_RTL).when(mGridLayout).getLayoutDirection();
+        doReturn(View.LAYOUT_DIRECTION_RTL).when(mGridLayout).getCurrentLayoutDirection();
 
         doReturn(RotationUtils.ROTATION_LANDSCAPE).when(mGridLayout).getCurrentRotation();
         assertEquals(true, mGridLayout.shouldReverseListItems());
@@ -185,123 +131,26 @@
         assertEquals(0f, mGridLayout.getAnimationOffsetY(), .01);
     }
 
-    @Test(expected = IllegalStateException.class)
-    public void testOnUpdateList_noAdapter() {
-        mGridLayout.setAdapter(null);
-        mGridLayout.updateList();
-    }
-
     @Test
-    public void testOnUpdateList_noItems() {
-        doReturn(0).when(mAdapter).countSeparatedItems();
-        doReturn(0).when(mAdapter).countListItems();
-        mGridLayout.updateList();
-
-        ViewGroup separatedView = mGridLayout.getSeparatedView();
-        ListGridLayout listView = mGridLayout.getListView();
-
-        assertEquals(0, separatedView.getChildCount());
-        assertEquals(View.GONE, separatedView.getVisibility());
-
-        verify(mListGrid, times(0)).addItem(any());
-    }
-
-    @Test
-    public void testOnUpdateList_resizesFirstSeparatedItem() {
-        doReturn(1).when(mAdapter).countSeparatedItems();
-        doReturn(0).when(mAdapter).countListItems();
+    public void testUpdateSeparatedItemSize() {
         View firstView = new View(mContext, null);
         View secondView = new View(mContext, null);
 
-        doReturn(firstView).when(mAdapter).getView(eq(0), any(), any());
-        doReturn(true).when(mAdapter).shouldBeSeparated(0);
+        ViewGroup separatedView = mGridLayout.getSeparatedView();
+        separatedView.addView(firstView);
 
-        mGridLayout.updateList();
+        mGridLayout.updateSeparatedItemSize();
 
         ViewGroup.LayoutParams childParams = firstView.getLayoutParams();
         assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, childParams.width);
         assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, childParams.height);
 
-        doReturn(2).when(mAdapter).countSeparatedItems();
-        doReturn(secondView).when(mAdapter).getView(eq(1), any(), any());
-        doReturn(true).when(mAdapter).shouldBeSeparated(1);
+        separatedView.addView(secondView);
 
-        mGridLayout.updateList();
+        mGridLayout.updateSeparatedItemSize();
 
         childParams = firstView.getLayoutParams();
         assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, childParams.width);
         assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, childParams.height);
-
-
-    }
-
-    @Test
-    public void testOnUpdateList_onlySeparatedItems() {
-        doReturn(1).when(mAdapter).countSeparatedItems();
-        doReturn(0).when(mAdapter).countListItems();
-        View testView = new View(mContext, null);
-        doReturn(testView).when(mAdapter).getView(eq(0), any(), any());
-        doReturn(true).when(mAdapter).shouldBeSeparated(0);
-
-        mGridLayout.updateList();
-
-        verify(mListGrid, times(0)).addItem(any());
-    }
-
-    @Test
-    public void testOnUpdateList_oneSeparatedOneList() {
-        doReturn(1).when(mAdapter).countSeparatedItems();
-        doReturn(1).when(mAdapter).countListItems();
-        View view1 = new View(mContext, null);
-        View view2 = new View(mContext, null);
-
-        doReturn(view1).when(mAdapter).getView(eq(0), any(), any());
-        doReturn(true).when(mAdapter).shouldBeSeparated(0);
-
-        doReturn(view2).when(mAdapter).getView(eq(1), any(), any());
-        doReturn(false).when(mAdapter).shouldBeSeparated(1);
-
-        mGridLayout.updateList();
-
-        ViewGroup separatedView = mGridLayout.getSeparatedView();
-
-        assertEquals(1, separatedView.getChildCount());
-        assertEquals(View.VISIBLE, separatedView.getVisibility());
-        assertEquals(view1, separatedView.getChildAt(0));
-
-        verify(mListGrid, times(1)).addItem(view2);
-    }
-
-    @Test
-    public void testOnUpdateList_fourInList() {
-        doReturn(0).when(mAdapter).countSeparatedItems();
-        doReturn(4).when(mAdapter).countListItems();
-        View view1 = new View(mContext, null);
-        View view2 = new View(mContext, null);
-        View view3 = new View(mContext, null);
-        View view4 = new View(mContext, null);
-
-        doReturn(view1).when(mAdapter).getView(eq(0), any(), any());
-        doReturn(false).when(mAdapter).shouldBeSeparated(0);
-
-        doReturn(view2).when(mAdapter).getView(eq(1), any(), any());
-        doReturn(false).when(mAdapter).shouldBeSeparated(1);
-
-        doReturn(view3).when(mAdapter).getView(eq(2), any(), any());
-        doReturn(false).when(mAdapter).shouldBeSeparated(2);
-
-        doReturn(view4).when(mAdapter).getView(eq(3), any(), any());
-        doReturn(false).when(mAdapter).shouldBeSeparated(3);
-
-        mGridLayout.updateList();
-
-        ViewGroup separatedView = mGridLayout.getSeparatedView();
-        assertEquals(0, separatedView.getChildCount());
-        assertEquals(View.GONE, separatedView.getVisibility());
-
-        verify(mListGrid, times(1)).addItem(view1);
-        verify(mListGrid, times(1)).addItem(view2);
-        verify(mListGrid, times(1)).addItem(view3);
-        verify(mListGrid, times(1)).addItem(view4);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsLayoutTest.java
new file mode 100644
index 0000000..16dcd65
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsLayoutTest.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2019 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.globalactions;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.testing.AndroidTestingRunner;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.MultiListLayout;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+/**
+ * Tests for {@link ListGridLayout}.
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class GlobalActionsLayoutTest extends SysuiTestCase {
+
+    private TestLayout mLayout;
+    private TestAdapter mAdapter;
+
+    private class TestAdapter extends MultiListLayout.MultiListAdapter {
+        @Override
+        public void onClickItem(int index) { }
+
+        @Override
+        public boolean onLongClickItem(int index) {
+            return true;
+        }
+
+        @Override
+        public int countSeparatedItems() {
+            return -1;
+        }
+
+        @Override
+        public int countListItems() {
+            return -1;
+        }
+
+        @Override
+        public boolean shouldBeSeparated(int position) {
+            return false;
+        }
+
+        @Override
+        public int getCount() {
+            return countSeparatedItems() + countListItems();
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return null;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return -1;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            return null;
+        }
+    }
+
+    private class TestLayout extends GlobalActionsLayout {
+        ArrayList<View> mSeparatedViews = new ArrayList<>();
+        ArrayList<View> mListViews = new ArrayList<>();
+        boolean mSeparatedViewVisible = false;
+
+        TestLayout(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        @Override
+        protected boolean shouldReverseListItems() {
+            return false;
+        }
+
+        @Override
+        public float getAnimationOffsetX() {
+            return 0;
+        }
+
+        @Override
+        public float getAnimationOffsetY() {
+            return 0;
+        }
+
+        @Override
+        protected void addToListView(View v, boolean reverse) {
+            if (reverse) {
+                mListViews.add(0, v);
+            } else {
+                mListViews.add(v);
+            }
+        }
+
+        @Override
+        protected void addToSeparatedView(View v, boolean reverse) {
+            if (reverse) {
+                mSeparatedViews.add(0, v);
+            } else {
+                mSeparatedViews.add(v);
+            }
+        }
+
+        @Override
+        protected void setSeparatedViewVisibility(boolean visible) {
+            mSeparatedViewVisible = visible;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mLayout = spy(new TestLayout(mContext, null));
+        mAdapter = spy(new TestAdapter());
+        mLayout.setAdapter(mAdapter);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testOnUpdateList_noAdapter() {
+        mLayout.setAdapter(null);
+        mLayout.updateList();
+    }
+
+    @Test
+    public void testOnUpdateList_noItems() {
+        doReturn(0).when(mAdapter).countSeparatedItems();
+        doReturn(0).when(mAdapter).countListItems();
+        mLayout.updateList();
+
+        assertEquals(0, mLayout.mSeparatedViews.size());
+        assertEquals(0, mLayout.mListViews.size());
+
+        assertEquals(false, mLayout.mSeparatedViewVisible);
+    }
+
+    @Test
+    public void testOnUpdateList_oneSeparatedOneList() {
+        doReturn(1).when(mAdapter).countSeparatedItems();
+        doReturn(1).when(mAdapter).countListItems();
+        View view1 = new View(mContext, null);
+        View view2 = new View(mContext, null);
+
+        doReturn(view1).when(mAdapter).getView(eq(0), any(), any());
+        doReturn(true).when(mAdapter).shouldBeSeparated(0);
+
+        doReturn(view2).when(mAdapter).getView(eq(1), any(), any());
+        doReturn(false).when(mAdapter).shouldBeSeparated(1);
+
+        mLayout.updateList();
+
+        assertEquals(1, mLayout.mSeparatedViews.size());
+        assertEquals(1, mLayout.mListViews.size());
+        assertEquals(view1, mLayout.mSeparatedViews.get(0));
+        assertEquals(view2, mLayout.mListViews.get(0));
+    }
+
+
+    @Test
+    public void testOnUpdateList_twoSeparatedItems() {
+        doReturn(2).when(mAdapter).countSeparatedItems();
+        doReturn(0).when(mAdapter).countListItems();
+        View view1 = new View(mContext, null);
+        View view2 = new View(mContext, null);
+
+        doReturn(view1).when(mAdapter).getView(eq(0), any(), any());
+        doReturn(true).when(mAdapter).shouldBeSeparated(0);
+        doReturn(view2).when(mAdapter).getView(eq(1), any(), any());
+        doReturn(true).when(mAdapter).shouldBeSeparated(1);
+
+        mLayout.updateList();
+
+        assertEquals(2, mLayout.mSeparatedViews.size());
+        assertEquals(0, mLayout.mListViews.size());
+
+        assertEquals(view1, mLayout.mSeparatedViews.get(0));
+        assertEquals(view2, mLayout.mSeparatedViews.get(1));
+
+        // if separated view has items in it, should be made visible
+        assertEquals(true, mLayout.mSeparatedViewVisible);
+    }
+
+    @Test
+    public void testOnUpdateList_twoSeparatedItems_reverse() {
+        doReturn(2).when(mAdapter).countSeparatedItems();
+        doReturn(0).when(mAdapter).countListItems();
+        doReturn(true).when(mLayout).shouldReverseListItems();
+        View view1 = new View(mContext, null);
+        View view2 = new View(mContext, null);
+
+        doReturn(view1).when(mAdapter).getView(eq(0), any(), any());
+        doReturn(true).when(mAdapter).shouldBeSeparated(0);
+
+        doReturn(view2).when(mAdapter).getView(eq(1), any(), any());
+        doReturn(true).when(mAdapter).shouldBeSeparated(1);
+
+        mLayout.updateList();
+
+        assertEquals(2, mLayout.mSeparatedViews.size());
+        assertEquals(0, mLayout.mListViews.size());
+
+        // separated view items are not reversed in current implementation, and this is intentional!
+        assertEquals(view1, mLayout.mSeparatedViews.get(0));
+        assertEquals(view2, mLayout.mSeparatedViews.get(1));
+    }
+
+    @Test
+    public void testOnUpdateList_fourInList() {
+        doReturn(0).when(mAdapter).countSeparatedItems();
+        doReturn(4).when(mAdapter).countListItems();
+        View view1 = new View(mContext, null);
+        View view2 = new View(mContext, null);
+        View view3 = new View(mContext, null);
+        View view4 = new View(mContext, null);
+
+        doReturn(view1).when(mAdapter).getView(eq(0), any(), any());
+        doReturn(false).when(mAdapter).shouldBeSeparated(0);
+
+        doReturn(view2).when(mAdapter).getView(eq(1), any(), any());
+        doReturn(false).when(mAdapter).shouldBeSeparated(1);
+
+        doReturn(view3).when(mAdapter).getView(eq(2), any(), any());
+        doReturn(false).when(mAdapter).shouldBeSeparated(2);
+
+        doReturn(view4).when(mAdapter).getView(eq(3), any(), any());
+        doReturn(false).when(mAdapter).shouldBeSeparated(3);
+
+        mLayout.updateList();
+
+        assertEquals(0, mLayout.mSeparatedViews.size());
+        assertEquals(4, mLayout.mListViews.size());
+        assertEquals(view1, mLayout.mListViews.get(0));
+        assertEquals(view2, mLayout.mListViews.get(1));
+        assertEquals(view3, mLayout.mListViews.get(2));
+        assertEquals(view4, mLayout.mListViews.get(3));
+    }
+
+    @Test
+    public void testOnUpdateList_fourInList_reverse() {
+        doReturn(0).when(mAdapter).countSeparatedItems();
+        doReturn(4).when(mAdapter).countListItems();
+        doReturn(true).when(mLayout).shouldReverseListItems();
+        View view1 = new View(mContext, null);
+        View view2 = new View(mContext, null);
+        View view3 = new View(mContext, null);
+        View view4 = new View(mContext, null);
+
+        doReturn(view1).when(mAdapter).getView(eq(0), any(), any());
+        doReturn(false).when(mAdapter).shouldBeSeparated(0);
+
+        doReturn(view2).when(mAdapter).getView(eq(1), any(), any());
+        doReturn(false).when(mAdapter).shouldBeSeparated(1);
+
+        doReturn(view3).when(mAdapter).getView(eq(2), any(), any());
+        doReturn(false).when(mAdapter).shouldBeSeparated(2);
+
+        doReturn(view4).when(mAdapter).getView(eq(3), any(), any());
+        doReturn(false).when(mAdapter).shouldBeSeparated(3);
+
+        mLayout.updateList();
+
+        assertEquals(0, mLayout.mSeparatedViews.size());
+        assertEquals(4, mLayout.mListViews.size());
+        assertEquals(view1, mLayout.mListViews.get(3));
+        assertEquals(view2, mLayout.mListViews.get(2));
+        assertEquals(view3, mLayout.mListViews.get(1));
+        assertEquals(view4, mLayout.mListViews.get(0));
+    }
+}