Merge changes from topic "258442632" into tm-qpr-dev

* changes:
  Add panel using TaskView
  Distinguish between selected panel or structure
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 ef49b9c..0000000
--- a/packages/SystemUI/res/layout-sw600dp/global_actions_controls_list_view.xml
+++ /dev/null
@@ -1,30 +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.
-  -->
-
-<com.android.systemui.globalactions.MinHeightScrollView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:scrollbars="none">
-  <LinearLayout
-      android:id="@+id/global_actions_controls_list"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:orientation="vertical"
-      android:layout_marginLeft="@dimen/global_actions_side_margin"
-      android:layout_marginRight="@dimen/global_actions_side_margin" />
-</com.android.systemui.globalactions.MinHeightScrollView>
\ 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 11a5665..e08e63b 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.
 -->
 
-<LinearLayout
+<FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/control_detail_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
 
-  <com.android.systemui.globalactions.MinHeightScrollView
-      android:layout_width="match_parent"
-      android:layout_height="0dp"
-      android:layout_weight="1"
-      android:orientation="vertical"
-      android:scrollbars="none">
 
     <LinearLayout
         android:id="@+id/global_actions_controls"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:clipChildren="false"
+        android:layout_height="match_parent"
         android:orientation="vertical"
-        android:clipToPadding="false"
         android:paddingHorizontal="@dimen/controls_padding_horizontal" />
 
-  </com.android.systemui.globalactions.MinHeightScrollView>
-</LinearLayout>
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml
index 9d01148..9efad22 100644
--- a/packages/SystemUI/res/layout/controls_with_favorites.xml
+++ b/packages/SystemUI/res/layout/controls_with_favorites.xml
@@ -18,7 +18,7 @@
 
   <LinearLayout
       android:layout_width="match_parent"
-      android:layout_height="match_parent"
+      android:layout_height="wrap_content"
       android:orientation="horizontal"
       android:layout_marginTop="@dimen/controls_top_margin"
       android:layout_marginBottom="@dimen/controls_header_bottom_margin">
@@ -71,5 +71,27 @@
         android:background="?android:attr/selectableItemBackgroundBorderless" />
   </LinearLayout>
 
-  <include layout="@layout/global_actions_controls_list_view" />
+  <ScrollView
+        android:id="@+id/controls_scroll_view"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:orientation="vertical"
+        android:clipChildren="true"
+        android:scrollbars="none">
+    <include layout="@layout/global_actions_controls_list_view" />
+
+  </ScrollView>
+
+  <FrameLayout
+      android:id="@+id/controls_panel"
+      android:layout_width="match_parent"
+      android:layout_height="0dp"
+      android:layout_weight="1"
+      android:layout_marginLeft="@dimen/global_actions_side_margin"
+      android:layout_marginRight="@dimen/global_actions_side_margin"
+      android:background="#ff0000"
+      android:padding="@dimen/global_actions_side_margin"
+      android:visibility="gone"
+      />
 </merge>
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 31fadb1..2f49c3f 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.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 @@
      */
     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 50ce9d4..bdfe1fb 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.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 @@
         )
     }
 
-    override fun getPreferredStructure(): StructureInfo {
-        return uiController.getPreferredStructure(getFavorites())
+    override fun getPreferredSelection(): SelectedItem {
+        return uiController.getPreferredSelectedItem(getFavorites())
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
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 34bfa13..c8090bf 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 @@
     val componentName: ComponentName,
     val structure: CharSequence,
     val controls: List<ControlInfo>
-)
+) {
+    companion object {
+        val EMPTY_COMPONENT = ComponentName("", "")
+        val EMPTY_STRUCTURE = StructureInfo(EMPTY_COMPONENT, "", mutableListOf())
+    }
+}
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 2389ad1..753d5ad 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 @@
                 val localeComparator = compareBy<ControlsServiceInfo, CharSequence>(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 d3b5d0e..bd704c1 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.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 @@
     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 @@
 
     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 c1cfbcb..f5c5905 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 @@
 
     /**
      * 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<*>
 
@@ -53,9 +53,43 @@
     )
 
     /**
-     * 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>): StructureInfo
+    fun getPreferredSelectedItem(structures: List<StructureInfo>): 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 6cb0e8b..4c8e1ac 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.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,18 +37,22 @@
 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
 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.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
@@ -56,16 +61,21 @@
 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
 
@@ -80,39 +90,34 @@
         val controlsListingController: Lazy<ControlsListingController>,
         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<TaskViewFactory>,
+        dumpManager: DumpManager
+) : ControlsUiController, Dumpable {
 
     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<ControlInfo>()
-        )
     }
 
-    private var selectedStructure: StructureInfo = EMPTY_STRUCTURE
+    private var selectedItem: SelectedItem = SelectedItem.EMPTY_SELECTION
     private lateinit var allStructures: List<StructureInfo>
     private val controlsById = mutableMapOf<ControlKey, ControlWithState>()
     private val controlViewsById = mutableMapOf<ControlKey, ControlViewHolder>()
     private lateinit var parent: ViewGroup
-    private lateinit var lastItems: List<SelectionItem>
     private var popup: ListPopupWindow? = null
     private var hidden = true
     private lateinit var onDismiss: Runnable
     private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow)
     private var retainCache = false
+    private var lastSelections = emptyList<SelectionItem>()
     private val sharedPreferences
         get() = userFileManager.getSharedPreferences(
             fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
@@ -120,6 +125,8 @@
             userId = userTracker.userId
         )
 
+    private var taskViewController: PanelTaskViewController? = null
+
     private val collator = Collator.getInstance(context.resources.configuration.locales[0])
     private val localeComparator = compareBy<SelectionItem, CharSequence>(collator) {
         it.getTitle()
@@ -128,10 +135,12 @@
     private val onSeedingComplete = Consumer<Boolean> {
         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)
     }
@@ -139,6 +148,10 @@
     private lateinit var activityContext: Context
     private lateinit var listingCallback: ControlsListingController.ControlsListingCallback
 
+    init {
+        dumpManager.registerDumpable(javaClass.name, this)
+    }
+
     private fun createCallback(
         onResult: (List<SelectionItem>) -> Unit
     ): ControlsListingController.ControlsListingCallback {
@@ -146,7 +159,15 @@
             override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
                 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 +181,13 @@
 
     override fun resolveActivity(): Class<*> {
         val allStructures = controlsController.get().getFavorites()
-        val selectedStructure = getPreferredStructure(allStructures)
+        val selected = getPreferredSelectedItem(allStructures)
+        val anyPanels = controlsListingController.get().getCurrentServices()
+                .none { it.panelActivity != null }
 
         return if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) {
             ControlsActivity::class.java
-        } else if (selectedStructure.controls.isEmpty() && allStructures.size <= 1) {
+        } else if (!selected.hasControls && allStructures.size <= 1 && !anyPanels) {
             ControlsProviderSelectorActivity::class.java
         } else {
             ControlsActivity::class.java
@@ -186,31 +209,49 @@
         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 !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 {
-            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)
     }
 
+    private fun initialView(items: List<SelectionItem>) {
+        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))
@@ -290,27 +331,90 @@
     private fun showControlsView(items: List<SelectionItem>) {
         controlViewsById.clear()
 
-        val itemsByComponent = items.associateBy { it.componentName }
-        val itemsWithStructure = mutableListOf<SelectionItem>()
-        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<SelectionItem>()
+        allStructures.mapNotNullTo(panelsAndStructures) {
             itemsByComponent.get(it.componentName)?.copy(structure = it.structure)
         }
-        itemsWithStructure.sortWith(localeComparator)
+        panelsAndStructures.addAll(panels)
 
-        val selectionItem = findSelectionItem(selectedStructure, 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() {
-        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<View>(R.id.controls_scroll_view).visibility = View.GONE
+        val container = parent.requireViewById<FrameLayout>(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<String>(context, R.layout.controls_more_item, items)
 
         val anchor = parent.requireViewById<ImageView>(R.id.controls_more)
@@ -331,7 +435,13 @@
                         ) {
                             when (pos) {
                                 // 0: Add Control
-                                0 -> startFavoritingActivity(selectedStructure)
+                                0 -> {
+                                    if (isPanel) {
+                                        startProviderSelectorActivity()
+                                    } else {
+                                        startFavoritingActivity(selectedStructure)
+                                    }
+                                }
                                 // 1: Edit controls
                                 1 -> startEditingActivity(selectedStructure)
                             }
@@ -353,6 +463,9 @@
             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
@@ -363,14 +476,21 @@
             // 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<ViewGroup>(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(
@@ -398,14 +518,20 @@
         })
     }
 
-    private fun createListView(selected: SelectionItem) {
-        val inflater = LayoutInflater.from(context)
+    private fun createControlsSpaceFrame() {
+        val inflater = LayoutInflater.from(activityContext)
         inflater.inflate(R.layout.controls_with_favorites, parent, true)
 
         parent.requireViewById<ImageView>(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)
 
@@ -453,35 +579,51 @@
         }
     }
 
-    override fun getPreferredStructure(structures: List<StructureInfo>): StructureInfo {
-        if (structures.isEmpty()) return EMPTY_STRUCTURE
+    override fun getPreferredSelectedItem(structures: List<StructureInfo>): 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 maybeUpdateSelectedItem(item: SelectionItem): Boolean {
+        val newSelection = if (item.isPanel) {
+            SelectedItem.PanelItem(item.appName, item.componentName)
+        } else {
+            SelectedItem.StructureItem(allStructures.firstOrNull {
+                it.structure == item.structure && it.componentName == item.componentName
+            } ?: EMPTY_STRUCTURE)
+        }
+        return if (newSelection != selectedItem ) {
+            selectedItem = newSelection
+            updatePreferences(selectedItem)
+            true
+        } else {
+            false
+        }
     }
 
     private fun switchAppOrStructure(item: SelectionItem) {
-        val newSelection = allStructures.first {
-            it.structure == item.structure && it.componentName == item.componentName
-        }
-
-        if (newSelection != selectedStructure) {
-            selectedStructure = newSelection
-            updatePreferences(selectedStructure)
+        if (maybeUpdateSelectedItem(item)) {
             reload(parent)
         }
     }
@@ -505,6 +647,8 @@
 
         closeDialogs(true)
         controlsController.get().unsubscribe()
+        taskViewController?.dismiss()
+        taskViewController = null
 
         parent.removeAllViews()
         controlsById.clear()
@@ -545,20 +689,46 @@
         return row
     }
 
-    private fun findSelectionItem(si: StructureInfo, items: List<SelectionItem>): SelectionItem? =
-        items.firstOrNull {
-            it.componentName == si.componentName && it.structure == si.structure
+    private fun findSelectionItem(si: SelectedItem, items: List<SelectionItem>): SelectionItem? =
+        items.firstOrNull { it.matches(si) }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("ControlsUiControllerImpl:")
+        pw.asIndenting().indentIfPossible {
+            println("hidden: $hidden")
+            println("selectedItem: $selectedItem")
+            println("lastSelections: $lastSelections")
         }
+    }
 }
 
-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/controls/ui/PanelTaskViewController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
new file mode 100644
index 0000000..7143be2
--- /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 622fa65..0000000
--- 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/qs/tiles/DeviceControlsTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
index c65bd9b..41d8549 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.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 @@
         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/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
index e326611..6c66f0b 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 @@
                         // 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 1e4a9e4..765c4c0 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.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.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 @@
     fun testOnServicesUpdated_nullLoadLabel() {
         val captor = ArgumentCaptor
             .forClass(ControlsListingController.ControlsListingCallback::class.java)
-        val controlsServiceInfo = mock(ControlsServiceInfo::class.java)
+        val controlsServiceInfo = mock<ControlsServiceInfo>()
         val serviceInfo = listOf(controlsServiceInfo)
         `when`(controlsServiceInfo.loadLabel()).thenReturn(null)
         verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
@@ -81,4 +81,32 @@
 
         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 49c7442..e679b13 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.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.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,7 +50,7 @@
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.anyString
 import org.mockito.Mockito.mock
-import org.mockito.Mockito.times
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
@@ -63,16 +69,22 @@
     @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 },
@@ -82,12 +94,13 @@
                 Lazy { controlsListingController },
                 controlActionCoordinator,
                 activityStarter,
-                shadeController,
                 iconCache,
                 controlsMetricsLogger,
                 keyguardStateController,
                 userFileManager,
-                userTracker
+                userTracker,
+                Optional.of(taskViewFactory),
+                dumpManager
             )
         `when`(
                 userFileManager.getSharedPreferences(
@@ -105,8 +118,8 @@
     @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 +129,30 @@
 
     @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 +164,39 @@
             .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)
+    }
+
+    @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 0000000..5cd2ace
--- /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<TaskView.Listener>
+
+    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<Intent>()
+        val optionsCaptor = argumentCaptor<ActivityOptions>()
+
+        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()
+    }
+}
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 0000000..57176f0
--- /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 f7b9438e..e0b3125 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.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 @@
         `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 @@
                 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 @@
                 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()
@@ -257,6 +260,22 @@
     }
 
     @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(
             any(LifecycleOwner::class.java),
@@ -303,12 +322,12 @@
         )
         `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 @@
         `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))