diff --git a/packages/SystemUI/res/drawable/control_spinner_background.xml b/packages/SystemUI/res/drawable/control_spinner_background.xml
index 46a9dad29fecf..8416f9db93926 100644
--- a/packages/SystemUI/res/drawable/control_spinner_background.xml
+++ b/packages/SystemUI/res/drawable/control_spinner_background.xml
@@ -23,7 +23,7 @@
diff --git a/packages/SystemUI/res/drawable/controls_popup_bg.xml b/packages/SystemUI/res/drawable/controls_popup_bg.xml
new file mode 100644
index 0000000000000..34dd6e5f70d92
--- /dev/null
+++ b/packages/SystemUI/res/drawable/controls_popup_bg.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/drawable/controls_popup_item_background.xml b/packages/SystemUI/res/drawable/controls_popup_item_background.xml
new file mode 100644
index 0000000000000..799218007b395
--- /dev/null
+++ b/packages/SystemUI/res/drawable/controls_popup_item_background.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/layout/controls_spinner_item.xml b/packages/SystemUI/res/layout/controls_spinner_item.xml
index 574aed62ea867..4048d0371aa80 100644
--- a/packages/SystemUI/res/layout/controls_spinner_item.xml
+++ b/packages/SystemUI/res/layout/controls_spinner_item.xml
@@ -13,33 +13,28 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
+ android:layout_height="@dimen/control_popup_item_height"
+ android:background="@drawable/controls_popup_item_background"
+ android:gravity="center_vertical|start"
+ android:orientation="horizontal"
+ android:paddingStart="@dimen/control_popup_item_padding"
+ android:paddingEnd="@dimen/control_popup_item_padding">
-
+
+
-
-
-
-
-
-
+ tools:text="Android" />
diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml
index 71561c07ebd3a..b1259e48f7f7f 100644
--- a/packages/SystemUI/res/layout/controls_with_favorites.xml
+++ b/packages/SystemUI/res/layout/controls_with_favorites.xml
@@ -50,11 +50,9 @@
+ tools:text="@tools:sample/lorem" />
#33000000
@*android:color/black
#CC191C1D
+ #8A000000
#F6E388
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a52a2b7863bcb..88bed2e2d080d 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1200,6 +1200,13 @@
56dp
12dp
16dp
+ 4dp
+ 56dp
+ 16dp
+ 1dp
+ 380dp
+ 28dp
+ 16dp
24dp
20dp
14sp
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsPopupMenu.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsPopupMenu.kt
new file mode 100644
index 0000000000000..d08bc48e945d0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsPopupMenu.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 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.controls.ui
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
+import android.view.Gravity
+import android.view.View
+import android.widget.ListPopupWindow
+import android.widget.PopupWindow
+import com.android.systemui.R
+
+class ControlsPopupMenu(context: Context) : ListPopupWindow(context) {
+
+ private val resources: Resources = context.resources
+
+ private val listDividerHeight: Int =
+ resources.getDimensionPixelSize(R.dimen.control_popup_items_divider_height)
+ private val horizontalMargin: Int =
+ resources.getDimensionPixelSize(R.dimen.control_popup_horizontal_margin)
+ private val maxWidth: Int = resources.getDimensionPixelSize(R.dimen.control_popup_max_width)
+
+ private val dialogBackground: Drawable = resources.getDrawable(R.drawable.controls_popup_bg)!!
+ private val dimDrawable: Drawable = ColorDrawable(resources.getColor(R.color.control_popup_dim))
+
+ private var dismissListener: PopupWindow.OnDismissListener? = null
+
+ init {
+ setBackgroundDrawable(dialogBackground)
+
+ inputMethodMode = INPUT_METHOD_NOT_NEEDED
+ isModal = true
+ setDropDownGravity(Gravity.START)
+
+ // dismiss method isn't called when popup is hidden by outside touch. So we need to
+ // override a listener to remove a dimming foreground
+ super.setOnDismissListener {
+ anchorView?.rootView?.foreground = null
+ dismissListener?.onDismiss()
+ }
+ }
+
+ override fun show() {
+ // need to call show() first in order to construct the listView
+ super.show()
+
+ val paddedWidth = resources.displayMetrics.widthPixels - 2 * horizontalMargin
+ width = maxWidth.coerceAtMost(paddedWidth)
+ anchorView?.let {
+ horizontalOffset = -width / 2 + it.width / 2
+ verticalOffset = -it.height / 2
+ if (it.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
+ horizontalOffset = -horizontalOffset
+ }
+
+ it.rootView.foreground = dimDrawable
+ }
+
+ with(listView!!) {
+ clipToOutline = true
+ background = dialogBackground
+ dividerHeight = listDividerHeight
+ }
+
+ // actual show takes into account updated ListView specs
+ super.show()
+ }
+
+ override fun setOnDismissListener(listener: PopupWindow.OnDismissListener?) {
+ dismissListener = listener
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 09ba3738be34e..c20af074c71ed 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -604,7 +604,7 @@ class ControlsUiControllerImpl @Inject constructor (
setCompoundDrawablesRelative(selected.icon, null, null, null)
}
- val anchor = parent.requireViewById(R.id.controls_header)
+ val anchor = parent.requireViewById(R.id.app_or_structure_spinner)
if (items.size == 1) {
spinner.setBackground(null)
anchor.setOnClickListener(null)
@@ -617,10 +617,7 @@ class ControlsUiControllerImpl @Inject constructor (
anchor.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
- popup = GlobalActionsPopupMenu(
- popupThemedContext,
- true /* isDropDownMode */
- ).apply {
+ popup = ControlsPopupMenu(popupThemedContext).apply {
setAnchorView(anchor)
setAdapter(adapter)
@@ -868,22 +865,24 @@ internal data class SelectionItem(
}
}
-private class ItemAdapter(
- val parentContext: Context,
- val resource: Int
-) : ArrayAdapter(parentContext, resource) {
+private class ItemAdapter(parentContext: Context, val resource: Int) :
+ ArrayAdapter(parentContext, resource) {
- val layoutInflater = LayoutInflater.from(context)
+ private val layoutInflater = LayoutInflater.from(context)!!
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
- val item = getItem(position)
+ val item: SelectionItem = getItem(position)!!
val view = convertView ?: layoutInflater.inflate(resource, parent, false)
- view.requireViewById(R.id.controls_spinner_item).apply {
- setText(item.getTitle())
- }
- view.requireViewById(R.id.app_icon).apply {
- setImageDrawable(item.icon)
+ with(view.tag as? ViewHolder ?: ViewHolder(view).also { view.tag = it }) {
+ titleView.text = item.getTitle()
+ iconView.setImageDrawable(item.icon)
}
return view
}
+
+ private class ViewHolder(itemView: View) {
+
+ val titleView: TextView = itemView.requireViewById(R.id.controls_spinner_item)
+ val iconView: ImageView = itemView.requireViewById(R.id.app_icon)
+ }
}
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index 29680d8345362..302a53068fb04 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -138,6 +138,14 @@
android:finishOnCloseSystemDialogs="true"
android:excludeFromRecents="true" />
+
+
+
+
+
+
+
+ val listener = mock(OnDismissListener::class.java)
+ popupMenu.setOnDismissListener(listener)
+ popupMenu.show()
+
+ popupMenu.dismissImmediate()
+
+ verify(listener).onDismiss()
+ }
+
+ @Test
+ fun testPopupDoesntExceedMaxWidth() = testPopup { popupMenu ->
+ testDisplayMetrics.widthPixels = DISPLAY_WIDTH_WIDE
+
+ popupMenu.show()
+
+ assertThat(popupMenu.width).isEqualTo(MAX_WIDTH)
+ }
+
+ @Test
+ fun testPopupMarginsWidthLessMax() = testPopup { popupMenu ->
+ testDisplayMetrics.widthPixels = DISPLAY_WIDTH_NARROW
+
+ popupMenu.show()
+
+ assertThat(popupMenu.width).isEqualTo(DISPLAY_WIDTH_NARROW - 2 * HORIZONTAL_MARGIN)
+ }
+
+ private fun testPopup(test: (popup: ControlsPopupMenu) -> Unit) {
+ activityScenarioRule.scenario.onActivity { activity ->
+ val testActivity = setupActivity(activity)
+ test(ControlsPopupMenu(testActivity).apply { anchorView = View(testActivity) })
+ }
+ }
+
+ private fun setupActivity(real: Activity): Activity {
+ val resources =
+ spy(real.resources).apply {
+ whenever(getDimensionPixelSize(R.dimen.control_popup_items_divider_height))
+ .thenReturn(1)
+ whenever(getDimensionPixelSize(R.dimen.control_popup_horizontal_margin))
+ .thenReturn(HORIZONTAL_MARGIN)
+ whenever(getDimensionPixelSize(R.dimen.control_popup_max_width))
+ .thenReturn(MAX_WIDTH)
+ whenever(getDrawable(R.drawable.controls_popup_bg)).thenReturn(ShapeDrawable())
+ whenever(getColor(R.color.control_popup_dim)).thenReturn(Color.WHITE)
+ whenever(displayMetrics).thenAnswer { testDisplayMetrics }
+ }
+
+ return spy(real).also { whenever(it.resources).thenReturn(resources) }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index 330a1e457807f..845848071f549 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -234,7 +234,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
val serviceInfo2 = setUpPanel(panel2)
`when`(authorizedPanelsRepository.getAuthorizedPanels())
- .thenReturn(setOf(packageName1, packageName2))
+ .thenReturn(setOf(packageName1, packageName2))
underTest.show(parent, {}, context)
@@ -245,7 +245,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
captor.value.onServicesUpdated(listOf(serviceInfo1, serviceInfo2))
FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor)
- val header: View = parent.requireViewById(R.id.controls_header)
+ val header: View = parent.requireViewById(R.id.app_or_structure_spinner)
assertThat(header.isClickable).isTrue()
assertThat(header.hasOnClickListeners()).isTrue()
}
diff --git a/packages/SystemUI/tests/utils/AndroidManifest.xml b/packages/SystemUI/tests/utils/AndroidManifest.xml
deleted file mode 100644
index cbef5f6036ab0..0000000000000
--- a/packages/SystemUI/tests/utils/AndroidManifest.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/activity/EmptyTestActivity.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/EmptyTestActivity.kt
new file mode 100644
index 0000000000000..22ac3d72ae486
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/EmptyTestActivity.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 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.activity
+
+import android.app.Activity
+
+/**
+ * This activity does nothing. You can use it with [ActivityScenario] or [ActivityScenarioRule] to
+ * run activity-independent tests
+ */
+class EmptyTestActivity : Activity()