Merge "Add better a11y support in QS" into tm-dev
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
index 75d95e6..f78046d 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
@@ -15,7 +15,6 @@
 package com.android.systemui.plugins.qs;
 
 import android.view.View;
-import android.view.View.OnClickListener;
 
 import com.android.systemui.plugins.FragmentBase;
 import com.android.systemui.plugins.annotations.DependsOn;
@@ -34,7 +33,7 @@
 
     String ACTION = "com.android.systemui.action.PLUGIN_QS";
 
-    int VERSION = 13;
+    int VERSION = 14;
 
     String TAG = "QS";
 
@@ -68,7 +67,12 @@
     void setHeaderListening(boolean listening);
     void notifyCustomizeChanged();
     void setContainerController(QSContainerController controller);
-    void setExpandClickListener(OnClickListener onClickListener);
+
+    /**
+     * Provide an action to collapse if expanded or expand if collapsed.
+     * @param action
+     */
+    void setCollapseExpandAction(Runnable action);
 
     /**
      * Returns the height difference between the QSPanel container and the QuickQSPanel container
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java
index 9a9683d..a8999ff 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java
@@ -26,7 +26,7 @@
 @DependsOn(target = QSIconView.class)
 @DependsOn(target = QSTile.class)
 public abstract class QSTileView extends LinearLayout {
-    public static final int VERSION = 2;
+    public static final int VERSION = 3;
 
     public QSTileView(Context context) {
         super(context);
@@ -71,4 +71,7 @@
     public View getSecondaryLabel() {
         return null;
     }
+
+    /** Sets the index of this tile in its layout */
+    public abstract void setPosition(int position);
 }
diff --git a/packages/SystemUI/res/layout/qs_tile_label.xml b/packages/SystemUI/res/layout/qs_tile_label.xml
index 02c58e4..77523ec9 100644
--- a/packages/SystemUI/res/layout/qs_tile_label.xml
+++ b/packages/SystemUI/res/layout/qs_tile_label.xml
@@ -24,6 +24,8 @@
     android:orientation="vertical"
     android:layout_marginStart="@dimen/qs_label_container_margin"
     android:layout_marginEnd="0dp"
+    android:focusable="false"
+    android:importantForAccessibility="no"
     android:layout_gravity="center_vertical | start">
 
     <com.android.systemui.util.SafeMarqueeTextView
@@ -35,6 +37,8 @@
         android:ellipsize="marquee"
         android:marqueeRepeatLimit="1"
         android:singleLine="true"
+        android:focusable="false"
+        android:importantForAccessibility="no"
         android:textAppearance="@style/TextAppearance.QS.TileLabel"/>
 
     <com.android.systemui.util.SafeMarqueeTextView
@@ -47,6 +51,8 @@
         android:marqueeRepeatLimit="1"
         android:singleLine="true"
         android:visibility="gone"
+        android:focusable="false"
+        android:importantForAccessibility="no"
         android:textAppearance="@style/TextAppearance.QS.TileLabel.Secondary"
         android:textColor="?android:attr/textColorSecondary"/>
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index d20141b..34f771c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -15,6 +15,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.animation.Interpolator;
 import android.view.animation.OvershootInterpolator;
 import android.widget.Scroller;
@@ -552,6 +553,51 @@
         postInvalidateOnAnimation();
     }
 
+    private int sanitizePageAction(int action) {
+        int pageLeftId = AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT.getId();
+        int pageRightId = AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT.getId();
+        if (action == pageLeftId || action == pageRightId) {
+            if (!isLayoutRtl()) {
+                if (action == pageLeftId) {
+                    return AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
+                } else {
+                    return AccessibilityNodeInfo.ACTION_SCROLL_FORWARD;
+                }
+            } else {
+                if (action == pageLeftId) {
+                    return AccessibilityNodeInfo.ACTION_SCROLL_FORWARD;
+                } else {
+                    return AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
+                }
+            }
+        }
+        return action;
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        action = sanitizePageAction(action);
+        boolean performed = super.performAccessibilityAction(action, arguments);
+        if (performed && (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
+                || action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)) {
+            requestAccessibilityFocus();
+        }
+        return performed;
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        // getCurrentItem does not respect RTL, so it works well together with page actions that
+        // use left/right positioning.
+        if (getCurrentItem() != 0) {
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT);
+        }
+        if (getCurrentItem() != mPages.size() - 1) {
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT);
+        }
+    }
+
     private static Animator setupBounceAnimator(View view, int ordinal) {
         view.setAlpha(0f);
         view.setScaleX(0f);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
index aac5672..bcf60d1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
@@ -48,10 +48,5 @@
      */
     void setKeyguardShowing(boolean keyguardShowing);
 
-    /**
-     * Sets the {@link android.view.View.OnClickListener to be used on elements that expend QS.
-     */
-    void setExpandClickListener(View.OnClickListener onClickListener);
-
     default void disable(int state1, int state2, boolean animate) {}
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java
index 6c0ca49..61905ae 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java
@@ -23,13 +23,11 @@
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Build;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.AttributeSet;
 import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 import android.widget.TextView;
 
@@ -168,23 +166,6 @@
         super.onDetachedFromWindow();
     }
 
-    @Override
-    public boolean performAccessibilityAction(int action, Bundle arguments) {
-        if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
-            if (mExpandClickListener != null) {
-                mExpandClickListener.onClick(null);
-                return true;
-            }
-        }
-        return super.performAccessibilityAction(action, arguments);
-    }
-
-    @Override
-    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
-        super.onInitializeAccessibilityNodeInfo(info);
-        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
-    }
-
     void disable(int state2) {
         final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
         if (disabled == mQsDisabled) return;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java
index bef4f43..0d29a1a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java
@@ -114,12 +114,6 @@
         mView.setKeyguardShowing();
     }
 
-    /** */
-    @Override
-    public void setExpandClickListener(View.OnClickListener onClickListener) {
-        mView.setExpandClickListener(onClickListener);
-    }
-
     @Override
     public void disable(int state1, int state2, boolean animate) {
         mView.disable(state2);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index b5e1c5e..795a606 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -31,7 +31,6 @@
 import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 
@@ -717,8 +716,9 @@
     }
 
     @Override
-    public void setExpandClickListener(OnClickListener onClickListener) {
-        mFooter.setExpandClickListener(onClickListener);
+    public void setCollapseExpandAction(Runnable action) {
+        mQSPanelController.setCollapseExpandAction(action);
+        mQuickQSPanelController.setCollapseExpandAction(action);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 11a36ad..8c4dedc 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -34,6 +34,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.LinearLayout;
 
 import androidx.annotation.VisibleForTesting;
@@ -62,6 +63,8 @@
     private final int mMediaTopMargin;
     private final int mMediaTotalBottomMargin;
 
+    private Runnable mCollapseExpandAction;
+
     /**
      * The index where the content starts that needs to be moved between parents
      */
@@ -678,6 +681,28 @@
         mShouldMoveMediaOnExpansion = shouldMoveMediaOnExpansion;
     }
 
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        if (action == AccessibilityNodeInfo.ACTION_EXPAND
+                || action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
+            if (mCollapseExpandAction != null) {
+                mCollapseExpandAction.run();
+                return true;
+            }
+        }
+        return super.performAccessibilityAction(action, arguments);
+    }
+
+    public void setCollapseExpandAction(Runnable action) {
+        mCollapseExpandAction = action;
+    }
+
     private class H extends Handler {
         private static final int ANNOUNCE_FOR_ACCESSIBILITY = 1;
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 74d1a3d..9c8fc47 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -424,6 +424,14 @@
         return mView.getBrightnessView();
     }
 
+    /**
+     * Set a listener to collapse/expand QS.
+     * @param action
+     */
+    public void setCollapseExpandAction(Runnable action) {
+        mView.setCollapseExpandAction(action);
+    }
+
     /** Sets whether we are currently on lock screen. */
     public void setIsOnKeyguard(boolean isOnKeyguard) {
         boolean isOnSplitShadeLockscreen = mShouldUseSplitNotificationShade && isOnKeyguard;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index f5ae019..3c95da8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -20,6 +20,7 @@
 import android.content.res.Configuration;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.LinearLayout;
 
 import com.android.internal.logging.UiEventLogger;
@@ -167,6 +168,14 @@
         return QSEvent.QQS_TILE_VISIBLE;
     }
 
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        // Remove the collapse action from QSPanel
+        info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
+        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+    }
+
     static class QQSSideLabelTileLayout extends SideLabelTileLayout {
 
         private boolean mLastSelected;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
index da82d2c..9ef90ec 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
@@ -8,6 +8,7 @@
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.annotation.Nullable;
 
@@ -240,6 +241,7 @@
             } else {
                 record.tileView.setLeftTopRightBottom(left, top, right, bottom);
             }
+            record.tileView.setPosition(i);
             mLastTileBottom = bottom;
         }
     }
@@ -296,4 +298,11 @@
             }
         }
     }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        info.setCollectionInfo(
+                new AccessibilityNodeInfo.CollectionInfo(mRecords.size(), 1, false));
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index a712ce2..72dad06 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -69,6 +69,12 @@
         internal const val TILE_STATE_RES_PREFIX = "tile_states_"
     }
 
+    private var _position: Int = INVALID
+
+    override fun setPosition(position: Int) {
+        _position = position
+    }
+
     override var heightOverride: Int = HeightOverrideable.NO_OVERRIDE
         set(value) {
             if (field == value) return
@@ -404,6 +410,10 @@
                 }
             }
         }
+        if (_position != INVALID) {
+            info.collectionItemInfo =
+                AccessibilityNodeInfo.CollectionItemInfo(_position, 1, 0, 1, false)
+        }
     }
 
     override fun toString(): String {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 20fc39d..6007323 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -239,7 +239,7 @@
 
     private final DozeParameters mDozeParameters;
     private final OnHeightChangedListener mOnHeightChangedListener = new OnHeightChangedListener();
-    private final OnClickListener mOnClickListener = new OnClickListener();
+    private final Runnable mCollapseExpandAction = new CollapseExpandAction();
     private final OnOverscrollTopChangedListener
             mOnOverscrollTopChangedListener =
             new OnOverscrollTopChangedListener();
@@ -3573,7 +3573,7 @@
         public void onFragmentViewCreated(String tag, Fragment fragment) {
             mQs = (QS) fragment;
             mQs.setPanelView(mHeightListener);
-            mQs.setExpandClickListener(mOnClickListener);
+            mQs.setCollapseExpandAction(mCollapseExpandAction);
             mQs.setHeaderClickable(isQsExpansionEnabled());
             mQs.setOverscrolling(mStackScrollerOverscrolling);
             mQs.setInSplitShade(mShouldUseSplitNotificationShade);
@@ -4216,9 +4216,9 @@
         }
     }
 
-    private class OnClickListener implements View.OnClickListener {
+    private class CollapseExpandAction implements Runnable {
         @Override
-        public void onClick(View v) {
+        public void run() {
             onQsExpansionStarted();
             if (mQsExpanded) {
                 flingSettings(0 /* vel */, FLING_COLLAPSE, null /* onFinishRunnable */,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index 63f8641..829445e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.Fragment;
@@ -304,6 +305,16 @@
         assertThat(mQsFragmentView.getY()).isEqualTo(-qsAbsoluteBottom);
     }
 
+    @Test
+    public void setCollapseExpandAction_passedToControllers() {
+        Runnable action = () -> {};
+        QSFragment fragment = resumeAndGetFragment();
+        fragment.setCollapseExpandAction(action);
+
+        verify(mQSPanelController).setCollapseExpandAction(action);
+        verify(mQuickQSPanelController).setCollapseExpandAction(action);
+    }
+
     @Override
     protected Fragment instantiate(Context context, String className, Bundle arguments) {
         MockitoAnnotations.initMocks(this);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
index 04bbd60..ba02a82 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -21,24 +21,19 @@
 import android.testing.TestableLooper.RunWithLooper
 import android.view.View
 import android.view.ViewGroup
+import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.FrameLayout
 import android.widget.LinearLayout
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.plugins.qs.QSTileView
-import com.android.systemui.qs.QSPanelControllerBase.TileRecord
-import com.android.systemui.qs.logging.QSLogger
-import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
-import org.mockito.Mockito.`when` as whenever
 
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper
@@ -47,22 +42,8 @@
     private lateinit var mTestableLooper: TestableLooper
     private lateinit var mQsPanel: QSPanel
 
-    @Mock
-    private lateinit var mHost: QSTileHost
-
-    @Mock
-    private lateinit var dndTile: QSTileImpl<*>
-
-    @Mock
-    private lateinit var mDndTileRecord: TileRecord
-
-    @Mock
-    private lateinit var mQSLogger: QSLogger
     private lateinit var mParentView: ViewGroup
 
-    @Mock
-    private lateinit var mQSTileView: QSTileView
-
     private lateinit var mFooter: View
 
     @Before
@@ -71,8 +52,6 @@
         MockitoAnnotations.initMocks(this)
         mTestableLooper = TestableLooper.get(this)
 
-        mDndTileRecord.tile = dndTile
-        mDndTileRecord.tileView = mQSTileView
         mTestableLooper.runWithLooper {
             mQsPanel = QSPanel(mContext, null)
             mQsPanel.initialize()
@@ -86,11 +65,6 @@
             mParentView = FrameLayout(mContext).apply {
                 addView(mQsPanel)
             }
-
-            whenever(dndTile.tileSpec).thenReturn("dnd")
-            whenever(mHost.tiles).thenReturn(emptyList())
-            whenever(mHost.createTileView(any(), any(), anyBoolean())).thenReturn(mQSTileView)
-            mQsPanel.addTile(mDndTileRecord)
         }
     }
 
@@ -137,6 +111,24 @@
         assertThat(mQsPanel.indexOfChild(mQsPanel.mSecurityFooter)).isEqualTo(-1)
     }
 
+    @Test
+    fun testHasCollapseAccessibilityAction() {
+        val info = AccessibilityNodeInfo(mQsPanel)
+        mQsPanel.onInitializeAccessibilityNodeInfo(info)
+
+        assertThat(info.actions and AccessibilityNodeInfo.ACTION_COLLAPSE).isNotEqualTo(0)
+        assertThat(info.actions and AccessibilityNodeInfo.ACTION_EXPAND).isEqualTo(0)
+    }
+
+    @Test
+    fun testCollapseActionCallsRunnable() {
+        val mockRunnable = mock(Runnable::class.java)
+        mQsPanel.setCollapseExpandAction(mockRunnable)
+
+        mQsPanel.performAccessibilityAction(AccessibilityNodeInfo.ACTION_COLLAPSE, null)
+        verify(mockRunnable).run()
+    }
+
     private fun getNewOrientationConfig(@Configuration.Orientation newOrientation: Int) =
             context.resources.configuration.apply { orientation = newOrientation }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
new file mode 100644
index 0000000..60c2bde
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
@@ -0,0 +1,66 @@
+package com.android.systemui.qs
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+@SmallTest
+class QuickQSPanelTest : SysuiTestCase() {
+
+    private lateinit var testableLooper: TestableLooper
+    private lateinit var quickQSPanel: QuickQSPanel
+
+    private lateinit var parentView: ViewGroup
+
+    @Before
+    @Throws(Exception::class)
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        testableLooper = TestableLooper.get(this)
+
+        testableLooper.runWithLooper {
+            quickQSPanel = QuickQSPanel(mContext, null)
+            quickQSPanel.initialize()
+
+            quickQSPanel.onFinishInflate()
+            quickQSPanel.setSecurityFooter(View(mContext), false)
+            quickQSPanel.setHeaderContainer(LinearLayout(mContext))
+            // Provides a parent with non-zero size for QSPanel
+            parentView = FrameLayout(mContext).apply {
+                addView(quickQSPanel)
+            }
+        }
+    }
+
+    @Test
+    fun testHasExpandAccessibilityAction() {
+        val info = AccessibilityNodeInfo(quickQSPanel)
+        quickQSPanel.onInitializeAccessibilityNodeInfo(info)
+
+        Truth.assertThat(info.actions and AccessibilityNodeInfo.ACTION_EXPAND).isNotEqualTo(0)
+        Truth.assertThat(info.actions and AccessibilityNodeInfo.ACTION_COLLAPSE).isEqualTo(0)
+    }
+
+    @Test
+    fun testExpandActionCallsRunnable() {
+        val mockRunnable = Mockito.mock(Runnable::class.java)
+        quickQSPanel.setCollapseExpandAction(mockRunnable)
+
+        quickQSPanel.performAccessibilityAction(AccessibilityNodeInfo.ACTION_EXPAND, null)
+        Mockito.verify(mockRunnable).run()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
index bd4bfff..5abc0e1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.verify;
 
 import android.test.suitebuilder.annotation.SmallTest;
+import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -170,4 +171,36 @@
         mTileLayout.measure(mLayoutSizeForOneTile, mLayoutSizeForOneTile);
         assertEquals(0, mTileLayout.getMeasuredHeight());
     }
+
+    @Test
+    public void testCollectionInfo() {
+        QSPanelControllerBase.TileRecord tileRecord1 = createTileRecord();
+        QSPanelControllerBase.TileRecord tileRecord2 = createTileRecord();
+        AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(mTileLayout);
+        mTileLayout.addTile(tileRecord1);
+
+        mTileLayout.onInitializeAccessibilityNodeInfo(info);
+        AccessibilityNodeInfo.CollectionInfo collectionInfo = info.getCollectionInfo();
+        assertEquals(1, collectionInfo.getRowCount());
+        assertEquals(1, collectionInfo.getColumnCount()); // always use one column
+
+        mTileLayout.addTile(tileRecord2);
+        mTileLayout.onInitializeAccessibilityNodeInfo(info);
+        collectionInfo = info.getCollectionInfo();
+        assertEquals(2, collectionInfo.getRowCount());
+        assertEquals(1, collectionInfo.getColumnCount()); // always use one column
+    }
+
+    @Test
+    public void testSetPositionOnTiles() {
+        QSPanelControllerBase.TileRecord tileRecord1 = createTileRecord();
+        QSPanelControllerBase.TileRecord tileRecord2 = createTileRecord();
+        mTileLayout.addTile(tileRecord1);
+        mTileLayout.addTile(tileRecord2);
+        mTileLayout.measure(mLayoutSizeForOneTile * 2, mLayoutSizeForOneTile * 2);
+        mTileLayout.layout(0, 0, mLayoutSizeForOneTile * 2, mLayoutSizeForOneTile * 2);
+
+        verify(tileRecord1.tileView).setPosition(0);
+        verify(tileRecord2.tileView).setPosition(1);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
index a2b5013..9fdc2fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
@@ -23,6 +23,7 @@
 import android.testing.TestableLooper
 import android.text.TextUtils
 import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
@@ -257,6 +258,28 @@
         assertThat((tileView.secondaryLabel as TextView).text).isEqualTo(onString)
     }
 
+    @Test
+    fun testCollectionItemInfoHasPosition() {
+        val position = 5
+        tileView.setPosition(position)
+
+        val info = AccessibilityNodeInfo(tileView)
+        tileView.onInitializeAccessibilityNodeInfo(info)
+
+        assertThat(info.collectionItemInfo.rowIndex).isEqualTo(position)
+        assertThat(info.collectionItemInfo.rowSpan).isEqualTo(1)
+        assertThat(info.collectionItemInfo.columnIndex).isEqualTo(0)
+        assertThat(info.collectionItemInfo.columnSpan).isEqualTo(1)
+    }
+
+    @Test
+    fun testCollectionItemInfoNoPosition() {
+        val info = AccessibilityNodeInfo(tileView)
+        tileView.onInitializeAccessibilityNodeInfo(info)
+
+        assertThat(info.collectionItemInfo).isNull()
+    }
+
     class FakeTileView(
         context: Context,
         icon: QSIconView,