From 82587b1a426f84544568303d67f859f2aaad6037 Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Thu, 10 Nov 2022 15:48:00 -0500 Subject: [PATCH 1/2] Distinguish between selected panel or structure To pave the way, we add a SelectedItem that could contain a StructureInfo or information for a Panel. In particular, a Panel points to the ComponentName of the ControlsProviderService (and not of the activity) for backwards compatibility and migration. As we still need to match the SelectionInfo that has the activity information, this is not an issue. Also, add persistence for the panel using the same keys (and an extra boolean) for better migration. Test: atest com.android.systemui.controls Test: atest DeviceControlsTile Fixes: 258442632 Change-Id: I544f5cdcb4fdfe0488df05d686173ed3bb709b8e --- .../controls/controller/ControlsController.kt | 5 +- .../controller/ControlsControllerImpl.kt | 5 +- .../controls/controller/StructureInfo.kt | 7 +- .../controls/ui/ControlsUiController.kt | 40 +++++- .../controls/ui/ControlsUiControllerImpl.kt | 135 +++++++++++------- .../systemui/qs/tiles/DeviceControlsTile.kt | 10 +- .../ui/ControlsUiControllerImplTest.kt | 47 ++++-- .../systemui/controls/ui/SelectionItemTest.kt | 112 +++++++++++++++ .../qs/tiles/DeviceControlsTileTest.kt | 51 ++++--- 9 files changed, 321 insertions(+), 91 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/controls/ui/SelectionItemTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt index 31fadb13db08e..2f49c3fe863e1 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -24,6 +24,7 @@ import com.android.systemui.controls.ControlStatus import com.android.systemui.util.UserAwareController import com.android.systemui.controls.management.ControlsFavoritingActivity import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.controls.ui.SelectedItem import java.util.function.Consumer /** @@ -184,8 +185,8 @@ interface ControlsController : UserAwareController { */ fun countFavoritesForComponent(componentName: ComponentName): Int - /** See [ControlsUiController.getPreferredStructure]. */ - fun getPreferredStructure(): StructureInfo + /** See [ControlsUiController.getPreferredSelectedItem]. */ + fun getPreferredSelection(): SelectedItem /** * Interface for structure to pass data to [ControlsFavoritingActivity]. diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index 50ce9d4ec0f4d..bdfe1fbeb4b55 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -38,6 +38,7 @@ import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager @@ -556,8 +557,8 @@ class ControlsControllerImpl @Inject constructor ( ) } - override fun getPreferredStructure(): StructureInfo { - return uiController.getPreferredStructure(getFavorites()) + override fun getPreferredSelection(): SelectedItem { + return uiController.getPreferredSelectedItem(getFavorites()) } override fun dump(pw: PrintWriter, args: Array) { diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/StructureInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/StructureInfo.kt index 34bfa135f0c85..c8090bfb60e89 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/StructureInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/StructureInfo.kt @@ -31,4 +31,9 @@ data class StructureInfo( val componentName: ComponentName, val structure: CharSequence, val controls: List -) +) { + companion object { + val EMPTY_COMPONENT = ComponentName("", "") + val EMPTY_STRUCTURE = StructureInfo(EMPTY_COMPONENT, "", mutableListOf()) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index c1cfbcb0c2115..180e09e5aaf02 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -53,9 +53,43 @@ interface ControlsUiController { ) /** - * Returns the structure that is currently preferred by the user. + * Returns the element that is currently preferred by the user. * - * This structure will be the one that appears when the user first opens the controls activity. + * This element will be the one that appears when the user first opens the controls activity. */ - fun getPreferredStructure(structures: List): StructureInfo + fun getPreferredSelectedItem(structures: List): SelectedItem } + +sealed class SelectedItem { + + abstract val name: CharSequence + abstract val hasControls: Boolean + abstract val componentName: ComponentName + + /** + * Represents the currently selected item for a structure. + */ + data class StructureItem(val structure: StructureInfo) : SelectedItem() { + override val name: CharSequence = structure.structure + override val hasControls: Boolean = structure.controls.isNotEmpty() + override val componentName: ComponentName = structure.componentName + } + + /** + * Represents the currently selected item for a service that provides a panel activity. + * + * The [componentName] is that of the service, as that is the expected identifier that should + * not change (to always provide proper migration). + */ + data class PanelItem( + val appName: CharSequence, + override val componentName: + ComponentName + ) : SelectedItem() { + override val name: CharSequence = appName + override val hasControls: Boolean = true + } + companion object { + val EMPTY_SELECTION: SelectedItem = StructureItem(StructureInfo.EMPTY_STRUCTURE) + } +} \ No newline at end of file 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 6cb0e8b048693..905636f563d13 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -41,13 +41,14 @@ import android.widget.LinearLayout import android.widget.ListPopupWindow import android.widget.Space import android.widget.TextView +import androidx.annotation.VisibleForTesting import com.android.systemui.R import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.CustomIconCache -import com.android.systemui.controls.controller.ControlInfo import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo +import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_COMPONENT import com.android.systemui.controls.management.ControlAdapter import com.android.systemui.controls.management.ControlsEditingActivity import com.android.systemui.controls.management.ControlsFavoritingActivity @@ -90,24 +91,17 @@ class ControlsUiControllerImpl @Inject constructor ( companion object { private const val PREF_COMPONENT = "controls_component" - private const val PREF_STRUCTURE = "controls_structure" + private const val PREF_STRUCTURE_OR_APP_NAME = "controls_structure" + private const val PREF_IS_PANEL = "controls_is_panel" private const val FADE_IN_MILLIS = 200L - - private val EMPTY_COMPONENT = ComponentName("", "") - private val EMPTY_STRUCTURE = StructureInfo( - EMPTY_COMPONENT, - "", - mutableListOf() - ) } - private var selectedStructure: StructureInfo = EMPTY_STRUCTURE + private var selectedItem: SelectedItem = SelectedItem.EMPTY_SELECTION private lateinit var allStructures: List private val controlsById = mutableMapOf() private val controlViewsById = mutableMapOf() private lateinit var parent: ViewGroup - private lateinit var lastItems: List private var popup: ListPopupWindow? = null private var hidden = true private lateinit var onDismiss: Runnable @@ -128,10 +122,12 @@ class ControlsUiControllerImpl @Inject constructor ( private val onSeedingComplete = Consumer { accepted -> if (accepted) { - selectedStructure = controlsController.get().getFavorites().maxByOrNull { + selectedItem = controlsController.get().getFavorites().maxByOrNull { it.controls.size - } ?: EMPTY_STRUCTURE - updatePreferences(selectedStructure) + }?.let { + SelectedItem.StructureItem(it) + } ?: SelectedItem.EMPTY_SELECTION + updatePreferences(selectedItem) } reload(parent) } @@ -146,7 +142,15 @@ class ControlsUiControllerImpl @Inject constructor ( override fun onServicesUpdated(serviceInfos: List) { val lastItems = serviceInfos.map { val uid = it.serviceInfo.applicationInfo.uid - SelectionItem(it.loadLabel(), "", it.loadIcon(), it.componentName, uid) + + SelectionItem( + it.loadLabel(), + "", + it.loadIcon(), + it.componentName, + uid, + it.panelActivity + ) } uiExecutor.execute { parent.removeAllViews() @@ -160,11 +164,11 @@ class ControlsUiControllerImpl @Inject constructor ( override fun resolveActivity(): Class<*> { val allStructures = controlsController.get().getFavorites() - val selectedStructure = getPreferredStructure(allStructures) + val selected = getPreferredSelectedItem(allStructures) return if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { ControlsActivity::class.java - } else if (selectedStructure.controls.isEmpty() && allStructures.size <= 1) { + } else if (!selected.hasControls && allStructures.size <= 1) { ControlsProviderSelectorActivity::class.java } else { ControlsActivity::class.java @@ -186,21 +190,24 @@ class ControlsUiControllerImpl @Inject constructor ( controlActionCoordinator.activityContext = activityContext allStructures = controlsController.get().getFavorites() - selectedStructure = getPreferredStructure(allStructures) + selectedItem = getPreferredSelectedItem(allStructures) if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { listingCallback = createCallback(::showSeedingView) - } else if (selectedStructure.controls.isEmpty() && allStructures.size <= 1) { + } else if (!selectedItem.hasControls && allStructures.size <= 1) { // only show initial view if there are really no favorites across any structure listingCallback = createCallback(::showInitialSetupView) } else { - selectedStructure.controls.map { - ControlWithState(selectedStructure.componentName, it, null) - }.associateByTo(controlsById) { - ControlKey(selectedStructure.componentName, it.ci.controlId) + val selected = selectedItem + if (selected is SelectedItem.StructureItem) { + selected.structure.controls.map { + ControlWithState(selected.structure.componentName, it, null) + }.associateByTo(controlsById) { + ControlKey(selected.structure.componentName, it.ci.controlId) + } + controlsController.get().subscribeToFavorites(selected.structure) } listingCallback = createCallback(::showControlsView) - controlsController.get().subscribeToFavorites(selectedStructure) } controlsListingController.get().addCallback(listingCallback) @@ -297,7 +304,7 @@ class ControlsUiControllerImpl @Inject constructor ( } itemsWithStructure.sortWith(localeComparator) - val selectionItem = findSelectionItem(selectedStructure, itemsWithStructure) ?: items[0] + val selectionItem = findSelectionItem(selectedItem, itemsWithStructure) ?: items[0] controlsMetricsLogger.refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked()) @@ -307,6 +314,8 @@ class ControlsUiControllerImpl @Inject constructor ( } private fun createMenu() { + if (selectedItem !is SelectedItem.StructureItem) return + val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure val items = arrayOf( context.resources.getString(R.string.controls_menu_add), context.resources.getString(R.string.controls_menu_edit) @@ -399,6 +408,8 @@ class ControlsUiControllerImpl @Inject constructor ( } private fun createListView(selected: SelectionItem) { + if (selectedItem !is SelectedItem.StructureItem) return + val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure val inflater = LayoutInflater.from(context) inflater.inflate(R.layout.controls_with_favorites, parent, true) @@ -453,35 +464,44 @@ class ControlsUiControllerImpl @Inject constructor ( } } - override fun getPreferredStructure(structures: List): StructureInfo { - if (structures.isEmpty()) return EMPTY_STRUCTURE + override fun getPreferredSelectedItem(structures: List): SelectedItem { + val sp = sharedPreferences - val component = sharedPreferences.getString(PREF_COMPONENT, null)?.let { + val component = sp.getString(PREF_COMPONENT, null)?.let { ComponentName.unflattenFromString(it) } ?: EMPTY_COMPONENT - val structure = sharedPreferences.getString(PREF_STRUCTURE, "") - - return structures.firstOrNull { - component == it.componentName && structure == it.structure - } ?: structures.get(0) + val name = sp.getString(PREF_STRUCTURE_OR_APP_NAME, "")!! + val isPanel = sp.getBoolean(PREF_IS_PANEL, false) + return if (isPanel) { + SelectedItem.PanelItem(name, component) + } else { + if (structures.isEmpty()) return SelectedItem.EMPTY_SELECTION + SelectedItem.StructureItem(structures.firstOrNull { + component == it.componentName && name == it.structure + } ?: structures.get(0)) + } } - private fun updatePreferences(si: StructureInfo) { - if (si == EMPTY_STRUCTURE) return + private fun updatePreferences(si: SelectedItem) { sharedPreferences.edit() - .putString(PREF_COMPONENT, si.componentName.flattenToString()) - .putString(PREF_STRUCTURE, si.structure.toString()) - .commit() + .putString(PREF_COMPONENT, si.componentName.flattenToString()) + .putString(PREF_STRUCTURE_OR_APP_NAME, si.name.toString()) + .putBoolean(PREF_IS_PANEL, si is SelectedItem.PanelItem) + .commit() } private fun switchAppOrStructure(item: SelectionItem) { - val newSelection = allStructures.first { - it.structure == item.structure && it.componentName == item.componentName + val newSelection = if (item.isPanel) { + SelectedItem.PanelItem(item.appName, item.componentName) + } else { + SelectedItem.StructureItem(allStructures.first { + it.structure == item.structure && it.componentName == item.componentName + }) } - if (newSelection != selectedStructure) { - selectedStructure = newSelection - updatePreferences(selectedStructure) + if (newSelection != selectedItem) { + selectedItem = newSelection + updatePreferences(selectedItem) reload(parent) } } @@ -545,20 +565,37 @@ class ControlsUiControllerImpl @Inject constructor ( return row } - private fun findSelectionItem(si: StructureInfo, items: List): SelectionItem? = - items.firstOrNull { - it.componentName == si.componentName && it.structure == si.structure - } + private fun findSelectionItem(si: SelectedItem, items: List): SelectionItem? = + items.firstOrNull { it.matches(si) } } -private data class SelectionItem( +@VisibleForTesting +internal data class SelectionItem( val appName: CharSequence, val structure: CharSequence, val icon: Drawable, val componentName: ComponentName, - val uid: Int + val uid: Int, + val panelComponentName: ComponentName? ) { fun getTitle() = if (structure.isEmpty()) { appName } else { structure } + + val isPanel: Boolean = panelComponentName != null + + fun matches(selectedItem: SelectedItem): Boolean { + if (componentName != selectedItem.componentName) { + // Not the same component so they are not the same. + return false + } + if (isPanel || selectedItem is SelectedItem.PanelItem) { + // As they have the same component, if [this.isPanel] then we may be migrating from + // device controls API into panel. Want this to match, even if the selectedItem is not + // a panel. We don't want to match on app name because that can change with locale. + return true + } + // Return true if we find a structure with the correct name + return structure == (selectedItem as SelectedItem.StructureItem).structure.structure + } } private class ItemAdapter( diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt index c65bd9b4348e1..41d854969e20f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt @@ -32,6 +32,7 @@ import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter @@ -125,14 +126,15 @@ class DeviceControlsTile @Inject constructor( state.icon = icon if (controlsComponent.isEnabled() && hasControlsApps.get()) { if (controlsComponent.getVisibility() == AVAILABLE) { - val structureInfo = controlsComponent - .getControlsController().get().getPreferredStructure() - state.state = if (structureInfo.controls.isEmpty()) { + val selection = controlsComponent + .getControlsController().get().getPreferredSelection() + state.state = if (selection is SelectedItem.StructureItem && + selection.structure.controls.isEmpty()) { Tile.STATE_INACTIVE } else { Tile.STATE_ACTIVE } - val label = structureInfo.structure + val label = selection.name state.secondaryLabel = if (label == tileLabel) null else label } else { state.state = Tile.STATE_INACTIVE 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 49c7442b9708a..a86cb00b3a818 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 @@ -44,7 +44,6 @@ import org.mockito.Mock import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyString import org.mockito.Mockito.mock -import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -105,8 +104,8 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Test fun testGetPreferredStructure() { val structureInfo = mock(StructureInfo::class.java) - underTest.getPreferredStructure(listOf(structureInfo)) - verify(userFileManager, times(2)) + underTest.getPreferredSelectedItem(listOf(structureInfo)) + verify(userFileManager) .getSharedPreferences( fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, mode = 0, @@ -116,25 +115,30 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Test fun testGetPreferredStructure_differentUserId() { - val structureInfo = + val selectedItems = listOf( - StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()), - StructureInfo(ComponentName.unflattenFromString("pkg/.cls2"), "b", ArrayList()), + SelectedItem.StructureItem( + StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()) + ), + SelectedItem.StructureItem( + StructureInfo(ComponentName.unflattenFromString("pkg/.cls2"), "b", ArrayList()) + ), ) + val structures = selectedItems.map { it.structure } sharedPreferences .edit() - .putString("controls_component", structureInfo[0].componentName.flattenToString()) - .putString("controls_structure", structureInfo[0].structure.toString()) + .putString("controls_component", selectedItems[0].componentName.flattenToString()) + .putString("controls_structure", selectedItems[0].name.toString()) .commit() val differentSharedPreferences = FakeSharedPreferences() differentSharedPreferences .edit() - .putString("controls_component", structureInfo[1].componentName.flattenToString()) - .putString("controls_structure", structureInfo[1].structure.toString()) + .putString("controls_component", selectedItems[1].componentName.flattenToString()) + .putString("controls_structure", selectedItems[1].name.toString()) .commit() - val previousPreferredStructure = underTest.getPreferredStructure(structureInfo) + val previousPreferredStructure = underTest.getPreferredSelectedItem(structures) `when`( userFileManager.getSharedPreferences( @@ -146,10 +150,25 @@ class ControlsUiControllerImplTest : SysuiTestCase() { .thenReturn(differentSharedPreferences) `when`(userTracker.userId).thenReturn(1) - val currentPreferredStructure = underTest.getPreferredStructure(structureInfo) + val currentPreferredStructure = underTest.getPreferredSelectedItem(structures) - assertThat(previousPreferredStructure).isEqualTo(structureInfo[0]) - assertThat(currentPreferredStructure).isEqualTo(structureInfo[1]) + assertThat(previousPreferredStructure).isEqualTo(selectedItems[0]) + assertThat(currentPreferredStructure).isEqualTo(selectedItems[1]) assertThat(currentPreferredStructure).isNotEqualTo(previousPreferredStructure) } + + @Test + fun testGetPreferredPanel() { + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + sharedPreferences + .edit() + .putString("controls_component", panel.componentName.flattenToString()) + .putString("controls_structure", panel.appName.toString()) + .putBoolean("controls_is_panel", true) + .commit() + + val selected = underTest.getPreferredSelectedItem(emptyList()) + + assertThat(selected).isEqualTo(panel) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/SelectionItemTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/SelectionItemTest.kt new file mode 100644 index 0000000000000..57176f0eabe0f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/SelectionItemTest.kt @@ -0,0 +1,112 @@ +package com.android.systemui.controls.ui + +import android.content.ComponentName +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.controller.StructureInfo +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class SelectionItemTest : SysuiTestCase() { + + @Test + fun testMatchBadComponentName_false() { + val selectionItem = + SelectionItem( + appName = "app", + structure = "structure", + icon = mock(), + componentName = ComponentName("pkg", "cls"), + uid = 0, + panelComponentName = null + ) + + assertThat( + selectionItem.matches( + SelectedItem.StructureItem( + StructureInfo(ComponentName("", ""), "s", emptyList()) + ) + ) + ) + .isFalse() + assertThat(selectionItem.matches(SelectedItem.PanelItem("name", ComponentName("", "")))) + .isFalse() + } + + @Test + fun testMatchSameComponentName_panelSelected_true() { + val componentName = ComponentName("pkg", "cls") + + val selectionItem = + SelectionItem( + appName = "app", + structure = "structure", + icon = mock(), + componentName = componentName, + uid = 0, + panelComponentName = null + ) + assertThat(selectionItem.matches(SelectedItem.PanelItem("name", componentName))).isTrue() + } + + @Test + fun testMatchSameComponentName_panelSelection_true() { + val componentName = ComponentName("pkg", "cls") + + val selectionItem = + SelectionItem( + appName = "app", + structure = "structure", + icon = mock(), + componentName = componentName, + uid = 0, + panelComponentName = ComponentName("pkg", "panel") + ) + assertThat(selectionItem.matches(SelectedItem.PanelItem("name", componentName))).isTrue() + } + + @Test + fun testMatchSameComponentSameStructure_true() { + val componentName = ComponentName("pkg", "cls") + val structureName = "structure" + + val structureItem = + SelectedItem.StructureItem(StructureInfo(componentName, structureName, emptyList())) + + val selectionItem = + SelectionItem( + appName = "app", + structure = structureName, + icon = mock(), + componentName = componentName, + uid = 0, + panelComponentName = null + ) + assertThat(selectionItem.matches(structureItem)).isTrue() + } + + @Test + fun testMatchSameComponentDifferentStructure_false() { + val componentName = ComponentName("pkg", "cls") + val structureName = "structure" + + val structureItem = + SelectedItem.StructureItem(StructureInfo(componentName, structureName, emptyList())) + + val selectionItem = + SelectionItem( + appName = "app", + structure = "other", + icon = mock(), + componentName = componentName, + uid = 0, + panelComponentName = null + ) + assertThat(selectionItem.matches(structureItem)).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt index f7b9438e6535e..e0b3125fd62a8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt @@ -40,6 +40,7 @@ import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsActivity import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost @@ -118,8 +119,9 @@ class DeviceControlsTileTest : SysuiTestCase() { `when`(qsHost.context).thenReturn(spiedContext) `when`(qsHost.uiEventLogger).thenReturn(uiEventLogger) `when`(controlsComponent.isEnabled()).thenReturn(true) - `when`(controlsController.getPreferredStructure()) - .thenReturn(StructureInfo(ComponentName("pkg", "cls"), "structure", listOf())) + `when`(controlsController.getPreferredSelection()) + .thenReturn(SelectedItem.StructureItem( + StructureInfo(ComponentName("pkg", "cls"), "structure", listOf()))) secureSettings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 1) setupControlsComponent() @@ -226,12 +228,12 @@ class DeviceControlsTileTest : SysuiTestCase() { capture(listingCallbackCaptor) ) `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) - `when`(controlsController.getPreferredStructure()).thenReturn( - StructureInfo( + `when`(controlsController.getPreferredSelection()).thenReturn( + SelectedItem.StructureItem(StructureInfo( ComponentName("pkg", "cls"), "structure", listOf(ControlInfo("id", "title", "subtitle", 1)) - ) + )) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) @@ -247,8 +249,9 @@ class DeviceControlsTileTest : SysuiTestCase() { capture(listingCallbackCaptor) ) `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) - `when`(controlsController.getPreferredStructure()) - .thenReturn(StructureInfo(ComponentName("pkg", "cls"), "structure", listOf())) + `when`(controlsController.getPreferredSelection()) + .thenReturn(SelectedItem.StructureItem( + StructureInfo(ComponentName("pkg", "cls"), "structure", listOf()))) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) testableLooper.processAllMessages() @@ -256,6 +259,22 @@ class DeviceControlsTileTest : SysuiTestCase() { assertThat(tile.state.state).isEqualTo(Tile.STATE_INACTIVE) } + @Test + fun testStateActiveIfPreferredIsPanel() { + verify(controlsListingController).observe( + any(LifecycleOwner::class.java), + capture(listingCallbackCaptor) + ) + `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) + `when`(controlsController.getPreferredSelection()) + .thenReturn(SelectedItem.PanelItem("appName", ComponentName("pkg", "cls"))) + + listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) + testableLooper.processAllMessages() + + assertThat(tile.state.state).isEqualTo(Tile.STATE_ACTIVE) + } + @Test fun testStateInactiveIfLocked() { verify(controlsListingController).observe( @@ -303,12 +322,12 @@ class DeviceControlsTileTest : SysuiTestCase() { ) `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) `when`(controlsUiController.resolveActivity()).thenReturn(ControlsActivity::class.java) - `when`(controlsController.getPreferredStructure()).thenReturn( - StructureInfo( - ComponentName("pkg", "cls"), - "structure", - listOf(ControlInfo("id", "title", "subtitle", 1)) - ) + `when`(controlsController.getPreferredSelection()).thenReturn( + SelectedItem.StructureItem(StructureInfo( + ComponentName("pkg", "cls"), + "structure", + listOf(ControlInfo("id", "title", "subtitle", 1)) + )) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) @@ -334,12 +353,12 @@ class DeviceControlsTileTest : SysuiTestCase() { `when`(controlsComponent.getVisibility()) .thenReturn(ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK) `when`(controlsUiController.resolveActivity()).thenReturn(ControlsActivity::class.java) - `when`(controlsController.getPreferredStructure()).thenReturn( - StructureInfo( + `when`(controlsController.getPreferredSelection()).thenReturn( + SelectedItem.StructureItem(StructureInfo( ComponentName("pkg", "cls"), "structure", listOf(ControlInfo("id", "title", "subtitle", 1)) - ) + )) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) From 64fdc5c527d60c146b56be236949d8ae8765c337 Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Mon, 14 Nov 2022 15:28:08 -0500 Subject: [PATCH 2/2] Add panel using TaskView Adds support for panel view. Controls that have favorites should migrate seamlessly to Panels (see SelectionItem#matches). A couple notes: * Apps with panels will not participate in seeding * If there is at least one app with panel, we show that instead of initial view. * If there is at least one app with panel, we don't open the app picker from the tile. * We save the last selection immediately so selectedItem is always in sync with SharedPreferences. * If we are seeing a panel, the overflow menu doesn't show Edit, and Add directs to the app picker. * The panel is shown inside a Red rectangle. This is temporary for debugging purposes. * Apps with panels are not shown in the Provider selector activity Note that if the flag (2000), no ControlsServiceInfo will have a panel component, and therefore there will be no panels, so we default to old behavior Test: manual Test: atest com.android.systemui.controls.ui Test: disable flag and see that everything behaves correctly, including scroll view Test: atest AppAdapterTest Fixes: 243392556 Change-Id: If260e193e57bec8948581dd4b0fa2f29e610c866 --- .../global_actions_controls_list_view.xml | 30 --- .../res/layout/controls_fullscreen.xml | 15 +- .../res/layout/controls_with_favorites.xml | 26 ++- .../controls/management/AppAdapter.kt | 3 +- .../systemui/controls/ui/ControlsActivity.kt | 7 + .../controls/ui/ControlsUiController.kt | 2 +- .../controls/ui/ControlsUiControllerImpl.kt | 203 +++++++++++++++--- .../controls/ui/PanelTaskViewController.kt | 102 +++++++++ .../globalactions/MinHeightScrollView.java | 43 ---- .../policy/DeviceControlsControllerImpl.kt | 3 + .../controls/management/AppAdapterTest.kt | 34 ++- .../ui/ControlsUiControllerImplTest.kt | 32 ++- .../ui/PanelTaskViewControllerTest.kt | 154 +++++++++++++ 13 files changed, 525 insertions(+), 129 deletions(-) delete mode 100644 packages/SystemUI/res/layout-sw600dp/global_actions_controls_list_view.xml create mode 100644 packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/globalactions/MinHeightScrollView.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt diff --git a/packages/SystemUI/res/layout-sw600dp/global_actions_controls_list_view.xml b/packages/SystemUI/res/layout-sw600dp/global_actions_controls_list_view.xml deleted file mode 100644 index ef49b9c3b6adb..0000000000000 --- a/packages/SystemUI/res/layout-sw600dp/global_actions_controls_list_view.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - \ No newline at end of file diff --git a/packages/SystemUI/res/layout/controls_fullscreen.xml b/packages/SystemUI/res/layout/controls_fullscreen.xml index 11a566588738a..e08e63b39e59a 100644 --- a/packages/SystemUI/res/layout/controls_fullscreen.xml +++ b/packages/SystemUI/res/layout/controls_fullscreen.xml @@ -15,28 +15,19 @@ limitations under the License. --> - - - - + diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml index 9d011482d0114..9efad22694634 100644 --- a/packages/SystemUI/res/layout/controls_with_favorites.xml +++ b/packages/SystemUI/res/layout/controls_with_favorites.xml @@ -18,7 +18,7 @@ @@ -71,5 +71,27 @@ android:background="?android:attr/selectableItemBackgroundBorderless" /> - + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt index 2389ad134c55e..753d5addeb114 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt @@ -64,7 +64,8 @@ class AppAdapter( val localeComparator = compareBy(collator) { it.loadLabel() ?: "" } - listOfServices = serviceInfos.sortedWith(localeComparator) + listOfServices = serviceInfos.filter { it.panelActivity == null } + .sortedWith(localeComparator) uiExecutor.execute(::notifyDataSetChanged) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt index d3b5d0edd2221..bd704c1ff0861 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt @@ -27,10 +27,13 @@ import android.view.View import android.view.ViewGroup import android.view.WindowInsets import android.view.WindowInsets.Type +import android.view.WindowManager import androidx.activity.ComponentActivity import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.management.ControlsAnimations +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import javax.inject.Inject /** @@ -44,6 +47,7 @@ class ControlsActivity @Inject constructor( private val uiController: ControlsUiController, private val broadcastDispatcher: BroadcastDispatcher, private val dreamManager: IDreamManager, + private val featureFlags: FeatureFlags ) : ComponentActivity() { private lateinit var parent: ViewGroup @@ -52,6 +56,9 @@ class ControlsActivity @Inject constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (featureFlags.isEnabled(Flags.USE_APP_PANELS)) { + window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) + } setContentView(R.layout.controls_fullscreen) diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index 180e09e5aaf02..f5c5905779a14 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -35,7 +35,7 @@ interface ControlsUiController { /** * Returns the preferred activity to start, depending on if the user has favorited any - * controls. + * controls or whether there are any app providing panels. */ fun resolveActivity(): Class<*> 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 905636f563d13..4c8e1ac968f9a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.app.Activity import android.app.ActivityOptions +import android.app.PendingIntent import android.content.ComponentName import android.content.Context import android.content.Intent @@ -36,12 +37,14 @@ import android.view.animation.AccelerateInterpolator import android.view.animation.DecelerateInterpolator import android.widget.AdapterView import android.widget.ArrayAdapter +import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.ListPopupWindow import android.widget.Space import android.widget.TextView import androidx.annotation.VisibleForTesting +import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsServiceInfo @@ -49,6 +52,7 @@ import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_COMPONENT +import com.android.systemui.controls.controller.StructureInfo.Companion.EMPTY_STRUCTURE import com.android.systemui.controls.management.ControlAdapter import com.android.systemui.controls.management.ControlsEditingActivity import com.android.systemui.controls.management.ControlsFavoritingActivity @@ -57,16 +61,21 @@ import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager import com.android.systemui.globalactions.GlobalActionsPopupMenu import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker -import com.android.systemui.shade.ShadeController import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.asIndenting import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.indentIfPossible +import com.android.wm.shell.TaskViewFactory import dagger.Lazy +import java.io.PrintWriter import java.text.Collator +import java.util.Optional import java.util.function.Consumer import javax.inject.Inject @@ -81,13 +90,14 @@ class ControlsUiControllerImpl @Inject constructor ( val controlsListingController: Lazy, val controlActionCoordinator: ControlActionCoordinator, private val activityStarter: ActivityStarter, - private val shadeController: ShadeController, private val iconCache: CustomIconCache, private val controlsMetricsLogger: ControlsMetricsLogger, private val keyguardStateController: KeyguardStateController, private val userFileManager: UserFileManager, private val userTracker: UserTracker, -) : ControlsUiController { + private val taskViewFactory: Optional, + dumpManager: DumpManager +) : ControlsUiController, Dumpable { companion object { private const val PREF_COMPONENT = "controls_component" @@ -107,6 +117,7 @@ class ControlsUiControllerImpl @Inject constructor ( private lateinit var onDismiss: Runnable private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow) private var retainCache = false + private var lastSelections = emptyList() private val sharedPreferences get() = userFileManager.getSharedPreferences( fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, @@ -114,6 +125,8 @@ class ControlsUiControllerImpl @Inject constructor ( userId = userTracker.userId ) + private var taskViewController: PanelTaskViewController? = null + private val collator = Collator.getInstance(context.resources.configuration.locales[0]) private val localeComparator = compareBy(collator) { it.getTitle() @@ -135,6 +148,10 @@ class ControlsUiControllerImpl @Inject constructor ( private lateinit var activityContext: Context private lateinit var listingCallback: ControlsListingController.ControlsListingCallback + init { + dumpManager.registerDumpable(javaClass.name, this) + } + private fun createCallback( onResult: (List) -> Unit ): ControlsListingController.ControlsListingCallback { @@ -165,10 +182,12 @@ class ControlsUiControllerImpl @Inject constructor ( override fun resolveActivity(): Class<*> { val allStructures = controlsController.get().getFavorites() val selected = getPreferredSelectedItem(allStructures) + val anyPanels = controlsListingController.get().getCurrentServices() + .none { it.panelActivity != null } return if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { ControlsActivity::class.java - } else if (!selected.hasControls && allStructures.size <= 1) { + } else if (!selected.hasControls && allStructures.size <= 1 && !anyPanels) { ControlsProviderSelectorActivity::class.java } else { ControlsActivity::class.java @@ -194,9 +213,13 @@ class ControlsUiControllerImpl @Inject constructor ( if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { listingCallback = createCallback(::showSeedingView) - } else if (!selectedItem.hasControls && allStructures.size <= 1) { + } else if ( + selectedItem !is SelectedItem.PanelItem && + !selectedItem.hasControls && + allStructures.size <= 1 + ) { // only show initial view if there are really no favorites across any structure - listingCallback = createCallback(::showInitialSetupView) + listingCallback = createCallback(::initialView) } else { val selected = selectedItem if (selected is SelectedItem.StructureItem) { @@ -213,11 +236,22 @@ class ControlsUiControllerImpl @Inject constructor ( controlsListingController.get().addCallback(listingCallback) } + private fun initialView(items: List) { + if (items.any { it.isPanel }) { + // We have at least a panel, so we'll end up showing that. + showControlsView(items) + } else { + showInitialSetupView(items) + } + } + private fun reload(parent: ViewGroup) { if (hidden) return controlsListingController.get().removeCallback(listingCallback) controlsController.get().unsubscribe() + taskViewController?.dismiss() + taskViewController = null val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f) fadeAnim.setInterpolator(AccelerateInterpolator(1.0f)) @@ -297,29 +331,90 @@ class ControlsUiControllerImpl @Inject constructor ( private fun showControlsView(items: List) { controlViewsById.clear() - val itemsByComponent = items.associateBy { it.componentName } - val itemsWithStructure = mutableListOf() - allStructures.mapNotNullTo(itemsWithStructure) { + val (panels, structures) = items.partition { it.isPanel } + val panelComponents = panels.map { it.componentName }.toSet() + + val itemsByComponent = structures.associateBy { it.componentName } + .filterNot { it.key in panelComponents } + val panelsAndStructures = mutableListOf() + allStructures.mapNotNullTo(panelsAndStructures) { itemsByComponent.get(it.componentName)?.copy(structure = it.structure) } - itemsWithStructure.sortWith(localeComparator) + panelsAndStructures.addAll(panels) - val selectionItem = findSelectionItem(selectedItem, itemsWithStructure) ?: items[0] + panelsAndStructures.sortWith(localeComparator) - controlsMetricsLogger.refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked()) + lastSelections = panelsAndStructures - createListView(selectionItem) - createDropDown(itemsWithStructure, selectionItem) + val selectionItem = findSelectionItem(selectedItem, panelsAndStructures) + ?: if (panels.isNotEmpty()) { + // If we couldn't find a good selected item, but there's at least one panel, + // show a panel. + panels[0] + } else { + items[0] + } + + maybeUpdateSelectedItem(selectionItem) + + createControlsSpaceFrame() + + if (taskViewFactory.isPresent && selectionItem.isPanel) { + createPanelView(selectionItem.panelComponentName!!) + } else if (!selectionItem.isPanel) { + controlsMetricsLogger + .refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked()) + createListView(selectionItem) + } else { + Log.w(ControlsUiController.TAG, "Not TaskViewFactory to display panel $selectionItem") + } + + createDropDown(panelsAndStructures, selectionItem) createMenu() } - private fun createMenu() { - if (selectedItem !is SelectedItem.StructureItem) return - val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure - val items = arrayOf( - context.resources.getString(R.string.controls_menu_add), - context.resources.getString(R.string.controls_menu_edit) + private fun createPanelView(componentName: ComponentName) { + val pendingIntent = PendingIntent.getActivity( + context, + 0, + Intent().setComponent(componentName), + PendingIntent.FLAG_IMMUTABLE ) + + parent.requireViewById(R.id.controls_scroll_view).visibility = View.GONE + val container = parent.requireViewById(R.id.controls_panel) + container.visibility = View.VISIBLE + container.post { + taskViewFactory.get().create(activityContext, uiExecutor) { taskView -> + taskViewController = PanelTaskViewController( + activityContext, + uiExecutor, + pendingIntent, + taskView, + onDismiss::run + ).also { + container.addView(taskView) + it.launchTaskView() + } + } + } + } + + private fun createMenu() { + val isPanel = selectedItem is SelectedItem.PanelItem + val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure + ?: EMPTY_STRUCTURE + + val items = if (isPanel) { + arrayOf( + context.resources.getString(R.string.controls_menu_add), + ) + } else { + arrayOf( + context.resources.getString(R.string.controls_menu_add), + context.resources.getString(R.string.controls_menu_edit) + ) + } var adapter = ArrayAdapter(context, R.layout.controls_more_item, items) val anchor = parent.requireViewById(R.id.controls_more) @@ -340,7 +435,13 @@ class ControlsUiControllerImpl @Inject constructor ( ) { when (pos) { // 0: Add Control - 0 -> startFavoritingActivity(selectedStructure) + 0 -> { + if (isPanel) { + startProviderSelectorActivity() + } else { + startFavoritingActivity(selectedStructure) + } + } // 1: Edit controls 1 -> startEditingActivity(selectedStructure) } @@ -362,6 +463,9 @@ class ControlsUiControllerImpl @Inject constructor ( addAll(items) } + val iconSize = context.resources + .getDimensionPixelSize(R.dimen.controls_header_app_icon_size) + /* * Default spinner widget does not work with the window type required * for this dialog. Use a textView with the ListPopupWindow to achieve @@ -372,14 +476,21 @@ class ControlsUiControllerImpl @Inject constructor ( // override the default color on the dropdown drawable (getBackground() as LayerDrawable).getDrawable(0) .setTint(context.resources.getColor(R.color.control_spinner_dropdown, null)) - } - - if (items.size == 1) { - spinner.setBackground(null) - return + selected.icon.setBounds(0, 0, iconSize, iconSize) + compoundDrawablePadding = (iconSize / 2.4f).toInt() + setCompoundDrawablesRelative(selected.icon, null, null, null) } val anchor = parent.requireViewById(R.id.controls_header) + if (items.size == 1) { + spinner.setBackground(null) + anchor.setOnClickListener(null) + return + } else { + spinner.background = parent.context.resources + .getDrawable(R.drawable.control_spinner_background) + } + anchor.setOnClickListener(object : View.OnClickListener { override fun onClick(v: View) { popup = GlobalActionsPopupMenu( @@ -407,16 +518,20 @@ class ControlsUiControllerImpl @Inject constructor ( }) } - private fun createListView(selected: SelectionItem) { - if (selectedItem !is SelectedItem.StructureItem) return - val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure - val inflater = LayoutInflater.from(context) + private fun createControlsSpaceFrame() { + val inflater = LayoutInflater.from(activityContext) inflater.inflate(R.layout.controls_with_favorites, parent, true) parent.requireViewById(R.id.controls_close).apply { setOnClickListener { _: View -> onDismiss.run() } visibility = View.VISIBLE } + } + + private fun createListView(selected: SelectionItem) { + if (selectedItem !is SelectedItem.StructureItem) return + val selectedStructure = (selectedItem as SelectedItem.StructureItem).structure + val inflater = LayoutInflater.from(activityContext) val maxColumns = ControlAdapter.findMaxColumns(activityContext.resources) @@ -490,18 +605,25 @@ class ControlsUiControllerImpl @Inject constructor ( .commit() } - private fun switchAppOrStructure(item: SelectionItem) { + private fun maybeUpdateSelectedItem(item: SelectionItem): Boolean { val newSelection = if (item.isPanel) { SelectedItem.PanelItem(item.appName, item.componentName) } else { - SelectedItem.StructureItem(allStructures.first { + SelectedItem.StructureItem(allStructures.firstOrNull { it.structure == item.structure && it.componentName == item.componentName - }) + } ?: EMPTY_STRUCTURE) } - - if (newSelection != selectedItem) { + return if (newSelection != selectedItem ) { selectedItem = newSelection updatePreferences(selectedItem) + true + } else { + false + } + } + + private fun switchAppOrStructure(item: SelectionItem) { + if (maybeUpdateSelectedItem(item)) { reload(parent) } } @@ -525,6 +647,8 @@ class ControlsUiControllerImpl @Inject constructor ( closeDialogs(true) controlsController.get().unsubscribe() + taskViewController?.dismiss() + taskViewController = null parent.removeAllViews() controlsById.clear() @@ -567,6 +691,15 @@ class ControlsUiControllerImpl @Inject constructor ( private fun findSelectionItem(si: SelectedItem, items: List): SelectionItem? = items.firstOrNull { it.matches(si) } + + override fun dump(pw: PrintWriter, args: Array) { + pw.println("ControlsUiControllerImpl:") + pw.asIndenting().indentIfPossible { + println("hidden: $hidden") + println("selectedItem: $selectedItem") + println("lastSelections: $lastSelections") + } + } } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt new file mode 100644 index 0000000000000..7143be298a9db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 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.app.ActivityOptions +import android.app.ActivityTaskManager +import android.app.ActivityTaskManager.INVALID_TASK_ID +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import com.android.systemui.util.boundsOnScreen +import com.android.wm.shell.TaskView +import java.util.concurrent.Executor + +class PanelTaskViewController( + private val activityContext: Context, + private val uiExecutor: Executor, + private val pendingIntent: PendingIntent, + private val taskView: TaskView, + private val hide: () -> Unit = {} +) { + + private var detailTaskId = INVALID_TASK_ID + + private val fillInIntent = + Intent().apply { + // Apply flags to make behaviour match documentLaunchMode=always. + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + } + + private fun removeDetailTask() { + if (detailTaskId == INVALID_TASK_ID) return + ActivityTaskManager.getInstance().removeTask(detailTaskId) + detailTaskId = INVALID_TASK_ID + } + + private val stateCallback = + object : TaskView.Listener { + override fun onInitialized() { + + val options = + ActivityOptions.makeCustomAnimation( + activityContext, + 0 /* enterResId */, + 0 /* exitResId */ + ) + options.taskAlwaysOnTop = true + + taskView.post { + taskView.startActivity( + pendingIntent, + fillInIntent, + options, + taskView.boundsOnScreen + ) + } + } + + override fun onTaskRemovalStarted(taskId: Int) { + detailTaskId = INVALID_TASK_ID + dismiss() + } + + override fun onTaskCreated(taskId: Int, name: ComponentName?) { + detailTaskId = taskId + } + + override fun onReleased() { + removeDetailTask() + } + + override fun onBackPressedOnTaskRoot(taskId: Int) { + dismiss() + hide() + } + } + + fun dismiss() { + taskView.release() + } + + fun launchTaskView() { + taskView.setListener(uiExecutor, stateCallback) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/MinHeightScrollView.java b/packages/SystemUI/src/com/android/systemui/globalactions/MinHeightScrollView.java deleted file mode 100644 index 622fa658f1b0e..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/globalactions/MinHeightScrollView.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.globalactions; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ScrollView; - -/** - * When measured, this view sets the minimum height of its first child to be equal to its own - * target height. - * - * This ensures fall-through click handlers can be placed on this view's child component. - */ -public class MinHeightScrollView extends ScrollView { - public MinHeightScrollView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - View firstChild = getChildAt(0); - if (firstChild != null) { - firstChild.setMinimumHeight(MeasureSpec.getSize(heightMeasureSpec)); - } - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt index e3266115f7779..6c66f0bb1e475 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt @@ -140,6 +140,9 @@ public class DeviceControlsControllerImpl @Inject constructor( // is out of sync, perhaps through a device restore, and update the // preference addPackageToSeededSet(prefs, pkg) + } else if (it.panelActivity != null) { + // Do not seed for packages with panels + addPackageToSeededSet(prefs, pkg) } else { componentsToSeed.add(it.componentName) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt index 1e4a9e42cce13..765c4c0ac0f07 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -34,9 +35,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mock -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest @@ -70,7 +70,7 @@ class AppAdapterTest : SysuiTestCase() { fun testOnServicesUpdated_nullLoadLabel() { val captor = ArgumentCaptor .forClass(ControlsListingController.ControlsListingCallback::class.java) - val controlsServiceInfo = mock(ControlsServiceInfo::class.java) + val controlsServiceInfo = mock() val serviceInfo = listOf(controlsServiceInfo) `when`(controlsServiceInfo.loadLabel()).thenReturn(null) verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture()) @@ -81,4 +81,32 @@ class AppAdapterTest : SysuiTestCase() { assertThat(adapter.itemCount).isEqualTo(serviceInfo.size) } + + @Test + fun testOnServicesUpdatedDoesntHavePanels() { + val captor = ArgumentCaptor + .forClass(ControlsListingController.ControlsListingCallback::class.java) + val serviceInfo = listOf( + ControlsServiceInfo("no panel", null), + ControlsServiceInfo("panel", mock()) + ) + verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture()) + + captor.value.onServicesUpdated(serviceInfo) + backgroundExecutor.runAllReady() + uiExecutor.runAllReady() + + assertThat(adapter.itemCount).isEqualTo(1) + } + + fun ControlsServiceInfo( + label: CharSequence, + panelComponentName: ComponentName? = null + ): ControlsServiceInfo { + return mock { + `when`(this.loadLabel()).thenReturn(label) + `when`(this.panelActivity).thenReturn(panelComponentName) + `when`(this.loadIcon()).thenReturn(mock()) + } + } } 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 a86cb00b3a818..e679b1391c770 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 @@ -17,8 +17,10 @@ package com.android.systemui.controls.ui import android.content.ComponentName +import android.content.Context import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsMetricsLogger @@ -26,6 +28,7 @@ import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker @@ -34,9 +37,12 @@ import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any import com.android.systemui.util.time.FakeSystemClock +import com.android.wm.shell.TaskViewFactory import com.google.common.truth.Truth.assertThat import dagger.Lazy +import java.util.Optional import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -44,6 +50,7 @@ import org.mockito.Mock import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyString import org.mockito.Mockito.mock +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -62,16 +69,22 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var keyguardStateController: KeyguardStateController @Mock lateinit var userFileManager: UserFileManager @Mock lateinit var userTracker: UserTracker + @Mock lateinit var taskViewFactory: TaskViewFactory + @Mock lateinit var activityContext: Context + @Mock lateinit var dumpManager: DumpManager val sharedPreferences = FakeSharedPreferences() var uiExecutor = FakeExecutor(FakeSystemClock()) var bgExecutor = FakeExecutor(FakeSystemClock()) lateinit var underTest: ControlsUiControllerImpl + lateinit var parent: FrameLayout @Before fun setup() { MockitoAnnotations.initMocks(this) + parent = FrameLayout(mContext) + underTest = ControlsUiControllerImpl( Lazy { controlsController }, @@ -81,12 +94,13 @@ class ControlsUiControllerImplTest : SysuiTestCase() { Lazy { controlsListingController }, controlActionCoordinator, activityStarter, - shadeController, iconCache, controlsMetricsLogger, keyguardStateController, userFileManager, - userTracker + userTracker, + Optional.of(taskViewFactory), + dumpManager ) `when`( userFileManager.getSharedPreferences( @@ -171,4 +185,18 @@ class ControlsUiControllerImplTest : SysuiTestCase() { assertThat(selected).isEqualTo(panel) } + + @Test + fun testPanelDoesNotRefreshControls() { + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + sharedPreferences + .edit() + .putString("controls_component", panel.componentName.flattenToString()) + .putString("controls_structure", panel.appName.toString()) + .putBoolean("controls_is_panel", true) + .commit() + + underTest.show(parent, {}, activityContext) + verify(controlsController, never()).refreshStatus(any(), any()) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt new file mode 100644 index 0000000000000..5cd2ace4604a4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022 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.app.ActivityOptions +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.boundsOnScreen +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.android.wm.shell.TaskView +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class PanelTaskViewControllerTest : SysuiTestCase() { + + companion object { + val FAKE_BOUNDS = Rect(10, 20, 30, 40) + } + + @Mock private lateinit var activityContext: Context + @Mock private lateinit var taskView: TaskView + @Mock private lateinit var pendingIntent: PendingIntent + @Mock private lateinit var hideRunnable: () -> Unit + + @Captor private lateinit var listenerCaptor: ArgumentCaptor + + private lateinit var uiExecutor: FakeExecutor + private lateinit var underTest: PanelTaskViewController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever(taskView.boundsOnScreen).thenAnswer { (it.arguments[0] as Rect).set(FAKE_BOUNDS) } + whenever(taskView.post(any())).thenAnswer { + uiExecutor.execute(it.arguments[0] as Runnable) + true + } + + uiExecutor = FakeExecutor(FakeSystemClock()) + + underTest = + PanelTaskViewController( + activityContext, + uiExecutor, + pendingIntent, + taskView, + hideRunnable + ) + } + + @Test + fun testLaunchTaskViewAttachedListener() { + underTest.launchTaskView() + verify(taskView).setListener(eq(uiExecutor), any()) + } + + @Test + fun testTaskViewOnInitializeStartsActivity() { + underTest.launchTaskView() + verify(taskView).setListener(any(), capture(listenerCaptor)) + + listenerCaptor.value.onInitialized() + uiExecutor.runAllReady() + + val intentCaptor = argumentCaptor() + val optionsCaptor = argumentCaptor() + + verify(taskView) + .startActivity( + eq(pendingIntent), + /* fillInIntent */ capture(intentCaptor), + capture(optionsCaptor), + eq(FAKE_BOUNDS) + ) + + assertThat(intentCaptor.value.flags) + .isEqualTo(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_MULTIPLE_TASK) + assertThat(optionsCaptor.value.taskAlwaysOnTop).isTrue() + } + + @Test + fun testHideRunnableCalledWhenBackOnRoot() { + underTest.launchTaskView() + verify(taskView).setListener(any(), capture(listenerCaptor)) + + listenerCaptor.value.onBackPressedOnTaskRoot(0) + + verify(hideRunnable).invoke() + } + + @Test + fun testTaskViewReleasedOnDismiss() { + underTest.dismiss() + verify(taskView).release() + } + + @Test + fun testTaskViewReleasedOnBackOnRoot() { + underTest.launchTaskView() + verify(taskView).setListener(any(), capture(listenerCaptor)) + + listenerCaptor.value.onBackPressedOnTaskRoot(0) + verify(taskView).release() + } + + @Test + fun testOnTaskRemovalStarted() { + underTest.launchTaskView() + verify(taskView).setListener(any(), capture(listenerCaptor)) + + listenerCaptor.value.onTaskRemovalStarted(0) + verify(taskView).release() + } +}