Add Structure pages to management screens

Pages are sorted alphabetically and show all controls for that
structure. The name used is the structure name of the app name for the
empty/null structure.

Done button saves all favorites selected from all structures, not just
the visible one.

Test: manual
Fixes: 150707923

Change-Id: I8ae822ed5acb6340aa08acb0dc6f3a4e6bb2f2e4
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index e066230..0eadcc7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -46,6 +46,7 @@
         "SystemUIPluginLib",
         "SystemUISharedLib",
         "SettingsLib",
+        "androidx.viewpager2_viewpager2",
         "androidx.legacy_legacy-support-v4",
         "androidx.recyclerview_recyclerview",
         "androidx.preference_preference",
@@ -106,6 +107,7 @@
         "SystemUIPluginLib",
         "SystemUISharedLib",
         "SettingsLib",
+        "androidx.viewpager2_viewpager2",
         "androidx.legacy_legacy-support-v4",
         "androidx.recyclerview_recyclerview",
         "androidx.preference_preference",
diff --git a/packages/SystemUI/res/layout/controls_management.xml b/packages/SystemUI/res/layout/controls_management.xml
index 6533c18..34a966c 100644
--- a/packages/SystemUI/res/layout/controls_management.xml
+++ b/packages/SystemUI/res/layout/controls_management.xml
@@ -25,13 +25,40 @@
     android:paddingStart="@dimen/controls_management_side_padding"
     android:paddingEnd="@dimen/controls_management_side_padding" >
 
-    <TextView
-        android:id="@+id/title"
+    <LinearLayout
+        android:orientation="horizontal"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:textAppearance="?android:attr/textAppearanceLarge"
-        android:textSize="@dimen/controls_title_size"
-        android:textAlignment="center" />
+        android:gravity="center_vertical">
+
+        <FrameLayout
+            android:id="@+id/icon_frame"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="start|center_vertical"
+            android:minWidth="56dp"
+            android:visibility="gone"
+            android:paddingTop="@dimen/controls_app_icon_frame_top_padding"
+            android:paddingBottom="@dimen/controls_app_icon_frame_bottom_padding"
+            android:paddingEnd="@dimen/controls_app_icon_frame_side_padding"
+            android:paddingStart="@dimen/controls_app_icon_frame_side_padding" >
+
+            <ImageView
+                android:id="@android:id/icon"
+                android:layout_width="@dimen/controls_app_icon_size"
+                android:layout_height="@dimen/controls_app_icon_size" />
+        </FrameLayout>
+
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAppearance="?android:attr/textAppearanceLarge"
+            android:textSize="@dimen/controls_title_size"
+            android:textAlignment="center" />
+
+    </LinearLayout>
+
 
     <TextView
         android:id="@+id/subtitle"
@@ -41,19 +68,11 @@
         android:textAppearance="?android:attr/textAppearanceSmall"
         android:textAlignment="center" />
 
-    <androidx.core.widget.NestedScrollView
+    <ViewStub
+        android:id="@+id/stub"
         android:layout_width="match_parent"
         android:layout_height="0dp"
-        android:layout_weight="1"
-        android:orientation="vertical"
-        android:layout_marginTop="@dimen/controls_management_list_margin">
-
-        <ViewStub
-            android:id="@+id/stub"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"/>
-
-    </androidx.core.widget.NestedScrollView>
+        android:layout_weight="1"/>
 
     <FrameLayout
         android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/controls_management_apps.xml b/packages/SystemUI/res/layout/controls_management_apps.xml
index 2bab433..42d73f3 100644
--- a/packages/SystemUI/res/layout/controls_management_apps.xml
+++ b/packages/SystemUI/res/layout/controls_management_apps.xml
@@ -14,12 +14,18 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
-<androidx.recyclerview.widget.RecyclerView
+<androidx.core.widget.NestedScrollView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/list"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    >
+    android:layout_height="0dp"
+    android:layout_weight="1"
+    android:orientation="vertical"
+    android:layout_marginTop="@dimen/controls_management_list_margin">
 
-</androidx.recyclerview.widget.RecyclerView>
\ No newline at end of file
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+    />
+
+</androidx.core.widget.NestedScrollView>
diff --git a/packages/SystemUI/res/layout/controls_management_favorites.xml b/packages/SystemUI/res/layout/controls_management_favorites.xml
index aab32f4..d2ccfcb 100644
--- a/packages/SystemUI/res/layout/controls_management_favorites.xml
+++ b/packages/SystemUI/res/layout/controls_management_favorites.xml
@@ -17,7 +17,7 @@
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
+    android:layout_height="0dp"
     android:orientation="vertical">
 
     <TextView
@@ -29,11 +29,17 @@
         android:gravity="center_horizontal"
     />
 
-    <androidx.recyclerview.widget.RecyclerView
-        android:id="@+id/listAll"
-        android:layout_width="match_parent"
+    <com.android.systemui.controls.management.ManagementPageIndicator
+        android:id="@+id/structure_page_indicator"
+        android:layout_width="wrap_content"
         android:layout_height="match_parent"
+        android:layout_gravity="center"
         android:layout_marginTop="@dimen/controls_management_list_margin"
-        android:nestedScrollingEnabled="false"/>
+        android:visibility="gone" />
+
+    <androidx.viewpager2.widget.ViewPager2
+        android:id="@+id/structure_pager"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
 
 </LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/controls_structure_page.xml b/packages/SystemUI/res/layout/controls_structure_page.xml
new file mode 100644
index 0000000..2c7e168
--- /dev/null
+++ b/packages/SystemUI/res/layout/controls_structure_page.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<androidx.core.widget.NestedScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:layout_marginTop="@dimen/controls_management_list_margin">
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/listAll"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+    />
+
+</androidx.core.widget.NestedScrollView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 9437485..291db65 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1249,6 +1249,7 @@
     <dimen name="controls_app_icon_size">32dp</dimen>
     <dimen name="controls_app_icon_frame_side_padding">8dp</dimen>
     <dimen name="controls_app_icon_frame_top_padding">4dp</dimen>
+    <dimen name="controls_app_icon_frame_bottom_padding">@dimen/controls_app_icon_frame_top_padding</dimen>
     <dimen name="controls_app_bottom_margin">8dp</dimen>
     <dimen name="controls_app_text_padding">8dp</dimen>
     <dimen name="controls_app_divider_height">2dp</dimen>
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 50bd1ad..5e1ed58 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -253,17 +253,21 @@
                     }
 
                     override fun error(message: String) {
-                        val loadData = Favorites.getControlsForComponent(componentName).let {
-                            controls ->
+                        executor.execute {
+                            val loadData = Favorites.getControlsForComponent(componentName)
+                                .let { controls ->
                                 val keys = controls.map { it.controlId }
                                 createLoadDataObject(
-                                    controls.map { createRemovedStatus(componentName, it, false) },
-                                    keys,
-                                    true
+                                        controls.map {
+                                            createRemovedStatus(componentName, it, false)
+                                        },
+                                        keys,
+                                        true
                                 )
-                        }
+                            }
 
-                        dataCallback.accept(loadData)
+                            dataCallback.accept(loadData)
+                        }
                     }
                 }
         )
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
index c053517..01f9069 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
@@ -22,14 +22,20 @@
 import com.android.systemui.controls.controller.ControlInfo
 
 /**
- * This model is used to show all controls separated by zones.
+ * This model is used to show controls separated by zones.
  *
  * The model will sort the controls and zones in the following manner:
  *  * The zones will be sorted in a first seen basis
  *  * The controls in each zone will be sorted in a first seen basis.
  *
- * @property controls List of all controls as returned by loading
- * @property initialFavoriteIds sorted ids of favorite controls
+ *  The controls passed should belong to the same structure, as an instance of this model will be
+ *  created for each structure.
+ *
+ *  The list of favorite ids can contain ids for controls not passed to this model. Those will be
+ *  filtered out.
+ *
+ * @property controls List of controls as returned by loading
+ * @property initialFavoriteIds sorted ids of favorite controls.
  * @property noZoneString text to use as header for all controls that have blank or `null` zone.
  */
 class AllModel(
@@ -50,7 +56,10 @@
             }
         }
 
-    private val favoriteIds = initialFavoriteIds.toMutableList()
+    private val favoriteIds = run {
+        val ids = controls.mapTo(HashSet()) { it.control.controlId }
+        initialFavoriteIds.filter { it in ids }.toMutableList()
+    }
 
     override val elements: List<ElementWrapper> = createWrappers(controls)
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
index c21f724..179e9fb 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
@@ -42,7 +42,6 @@
  * @param onlyFavorites set to true to only display favorites instead of all controls
  */
 class ControlAdapter(
-    private val layoutInflater: LayoutInflater,
     private val elevation: Float
 ) : RecyclerView.Adapter<Holder>() {
 
@@ -60,6 +59,7 @@
     private var model: ControlsModel? = null
 
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
+        val layoutInflater = LayoutInflater.from(parent.context)
         return when (viewType) {
             TYPE_CONTROL -> {
                 ControlHolder(
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
index 471f9d3..04715ab 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
@@ -19,20 +19,24 @@
 import android.app.Activity
 import android.content.ComponentName
 import android.content.Intent
+import android.graphics.drawable.Drawable
 import android.os.Bundle
-import android.view.LayoutInflater
+import android.text.TextUtils
 import android.view.View
 import android.view.ViewStub
 import android.widget.Button
+import android.widget.ImageView
 import android.widget.TextView
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
 import com.android.systemui.R
 import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.controls.controller.StructureInfo
+import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.controller.ControlsControllerImpl
+import com.android.systemui.controls.controller.StructureInfo
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.PageIndicator
 import com.android.systemui.settings.CurrentUserTracker
+import java.text.Collator
 import java.util.concurrent.Executor
 import java.util.function.Consumer
 import javax.inject.Inject
@@ -40,6 +44,7 @@
 class ControlsFavoritingActivity @Inject constructor(
     @Main private val executor: Executor,
     private val controller: ControlsControllerImpl,
+    private val listingController: ControlsListingController,
     broadcastDispatcher: BroadcastDispatcher
 ) : Activity() {
 
@@ -48,12 +53,18 @@
         const val EXTRA_APP = "extra_app_label"
     }
 
-    private lateinit var recyclerViewAll: RecyclerView
-    private lateinit var adapterAll: ControlAdapter
-    private lateinit var statusText: TextView
-    private var model: ControlsModel? = null
     private var component: ComponentName? = null
-    private var structureName: CharSequence = ""
+    private var appName: CharSequence? = null
+
+    private lateinit var structurePager: ViewPager2
+    private lateinit var statusText: TextView
+    private lateinit var titleView: TextView
+    private lateinit var iconView: ImageView
+    private lateinit var iconFrame: View
+    private lateinit var pageIndicator: PageIndicator
+    private var listOfStructures = emptyList<StructureContainer>()
+
+    private lateinit var comparator: Comparator<StructureContainer>
 
     private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
         private val startingUser = controller.currentUserId
@@ -66,29 +77,117 @@
         }
     }
 
+    private val listingCallback = object : ControlsListingController.ControlsListingCallback {
+        private var icon: Drawable? = null
+
+        override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
+            val newIcon = serviceInfos.firstOrNull { it.componentName == component }?.loadIcon()
+            if (icon == newIcon) return
+            icon = newIcon
+            executor.execute {
+                if (icon != null) {
+                    iconView.setImageDrawable(icon)
+                }
+                iconFrame.visibility = if (icon != null) View.VISIBLE else View.GONE
+            }
+        }
+    }
+
     override fun onBackPressed() {
         finish()
     }
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
+        val collator = Collator.getInstance(resources.configuration.locales[0])
+        comparator = compareBy(collator) { it.structureName }
+        appName = intent.getCharSequenceExtra(EXTRA_APP)
+        component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)
+
+        bindViews()
+
+        setUpPager()
+
+        loadControls()
+
+        listingController.addCallback(listingCallback)
+
+        currentUserTracker.startTracking()
+    }
+
+    private fun loadControls() {
+        component?.let {
+            statusText.text = resources.getText(com.android.internal.R.string.loading)
+            val emptyZoneString = resources.getText(
+                    R.string.controls_favorite_other_zone_header)
+            controller.loadForComponent(it, Consumer { data ->
+                val allControls = data.allControls
+                val favoriteKeys = data.favoritesIds
+                val error = data.errorOnLoad
+                val controlsByStructure = allControls.groupBy { it.control.structure ?: "" }
+                listOfStructures = controlsByStructure.map {
+                    StructureContainer(it.key, AllModel(it.value, favoriteKeys, emptyZoneString))
+                }.sortedWith(comparator)
+                executor.execute {
+                    structurePager.adapter = StructureAdapter(listOfStructures)
+                    if (error) {
+                        statusText.text = resources.getText(R.string.controls_favorite_load_error)
+                    } else {
+                        statusText.visibility = View.GONE
+                    }
+                    pageIndicator.setNumPages(listOfStructures.size)
+                    pageIndicator.setLocation(0f)
+                    pageIndicator.visibility =
+                        if (listOfStructures.size > 1) View.VISIBLE else View.GONE
+                }
+            })
+        }
+    }
+
+    private fun setUpPager() {
+        structurePager.apply {
+            adapter = StructureAdapter(emptyList())
+            registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+                override fun onPageSelected(position: Int) {
+                    super.onPageSelected(position)
+                    val name = listOfStructures[position].structureName
+                    titleView.text = if (!TextUtils.isEmpty(name)) name else appName
+                }
+
+                override fun onPageScrolled(
+                    position: Int,
+                    positionOffset: Float,
+                    positionOffsetPixels: Int
+                ) {
+                    super.onPageScrolled(position, positionOffset, positionOffsetPixels)
+                    pageIndicator.setLocation(position + positionOffset)
+                }
+            })
+        }
+    }
+
+    private fun bindViews() {
         setContentView(R.layout.controls_management)
         requireViewById<ViewStub>(R.id.stub).apply {
             layoutResource = R.layout.controls_management_favorites
             inflate()
         }
 
-        val app = intent.getCharSequenceExtra(EXTRA_APP)
-        component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)
         statusText = requireViewById(R.id.status_message)
+        pageIndicator = requireViewById(R.id.structure_page_indicator)
 
-        setUpRecyclerView()
-
-        requireViewById<TextView>(R.id.title).text = app?.let { it }
-                ?: resources.getText(R.string.controls_favorite_default_title)
+        titleView = requireViewById<TextView>(R.id.title).apply {
+            text = appName ?: resources.getText(R.string.controls_favorite_default_title)
+        }
         requireViewById<TextView>(R.id.subtitle).text =
                 resources.getText(R.string.controls_favorite_subtitle)
+        iconView = requireViewById(com.android.internal.R.id.icon)
+        iconFrame = requireViewById(R.id.icon_frame)
+        structurePager = requireViewById<ViewPager2>(R.id.structure_pager)
+        bindButtons()
+    }
 
+    private fun bindButtons() {
         requireViewById<Button>(R.id.other_apps).apply {
             visibility = View.VISIBLE
             setOnClickListener {
@@ -98,64 +197,21 @@
 
         requireViewById<Button>(R.id.done).setOnClickListener {
             if (component == null) return@setOnClickListener
-            val favoritesForStorage = model?.favorites?.map {
-                it.build()
-            }
-            if (favoritesForStorage != null) {
-                controller.replaceFavoritesForStructure(StructureInfo(component!!, structureName,
+            listOfStructures.forEach {
+                val favoritesForStorage = it.model.favorites.map { it.build() }
+                controller.replaceFavoritesForStructure(StructureInfo(component!!, it.structureName,
                         favoritesForStorage))
-                finishAffinity()
             }
-        }
 
-        component?.let {
-            statusText.text = resources.getText(com.android.internal.R.string.loading)
-            controller.loadForComponent(it, Consumer { data ->
-                val allControls = data.allControls
-                val favoriteKeys = data.favoritesIds
-                val error = data.errorOnLoad
-                val structures = allControls.fold(hashSetOf<CharSequence>()) {
-                    s, c ->
-                        s.add(c.control.structure ?: "")
-                        s
-                }
-                // TODO add multi structure switching support
-                executor.execute {
-                    val emptyZoneString = resources.getText(
-                            R.string.controls_favorite_other_zone_header)
-                    val model = AllModel(allControls, favoriteKeys, emptyZoneString)
-                    adapterAll.changeModel(model)
-                    this.model = model
-                    if (error) {
-                        statusText.text = resources.getText(R.string.controls_favorite_load_error)
-                    } else {
-                        statusText.visibility = View.GONE
-                    }
-                }
-            })
-        }
-
-        currentUserTracker.startTracking()
-    }
-
-    private fun setUpRecyclerView() {
-        val margin = resources.getDimensionPixelSize(R.dimen.controls_card_margin)
-        val itemDecorator = MarginItemDecorator(margin, margin)
-        val layoutInflater = LayoutInflater.from(applicationContext)
-        val elevation = resources.getFloat(R.dimen.control_card_elevation)
-
-        adapterAll = ControlAdapter(layoutInflater, elevation)
-        recyclerViewAll = requireViewById<RecyclerView>(R.id.listAll).apply {
-            adapter = adapterAll
-            layoutManager = GridLayoutManager(applicationContext, 2).apply {
-                spanSizeLookup = adapterAll.spanSizeLookup
-            }
-            addItemDecoration(itemDecorator)
+            finishAffinity()
         }
     }
 
     override fun onDestroy() {
         currentUserTracker.stopTracking()
+        listingController.removeCallback(listingCallback)
         super.onDestroy()
     }
 }
+
+data class StructureContainer(val structureName: CharSequence, val model: ControlsModel)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ManagementPageIndicator.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ManagementPageIndicator.kt
new file mode 100644
index 0000000..4289274
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ManagementPageIndicator.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.controls.management
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import com.android.systemui.qs.PageIndicator
+
+/**
+ * Page indicator for management screens.
+ *
+ * Adds RTL support to [PageIndicator]. To be used with [ViewPager2].
+ */
+class ManagementPageIndicator(
+    context: Context,
+    attrs: AttributeSet
+) : PageIndicator(context, attrs) {
+
+    override fun setLocation(location: Float) {
+        // Location doesn't know about RTL
+        if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
+            val numPages = childCount
+            super.setLocation(numPages - 1 - location)
+        } else {
+            super.setLocation(location)
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/StructureAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/StructureAdapter.kt
new file mode 100644
index 0000000..cb67454
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/StructureAdapter.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.controls.management
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.systemui.R
+
+class StructureAdapter(
+    private val models: List<StructureContainer>
+) : RecyclerView.Adapter<StructureAdapter.StructureHolder>() {
+
+    override fun onCreateViewHolder(parent: ViewGroup, p1: Int): StructureHolder {
+        val layoutInflater = LayoutInflater.from(parent.context)
+        return StructureHolder(
+            layoutInflater.inflate(R.layout.controls_structure_page, parent, false)
+        )
+    }
+
+    override fun getItemCount() = models.size
+
+    override fun onBindViewHolder(holder: StructureHolder, index: Int) {
+        holder.bind(models[index].model)
+    }
+
+    class StructureHolder(view: View) : RecyclerView.ViewHolder(view) {
+
+        private val recyclerView: RecyclerView
+        private val controlAdapter: ControlAdapter
+
+        init {
+            recyclerView = itemView.requireViewById<RecyclerView>(R.id.listAll)
+            val elevation = itemView.context.resources.getFloat(R.dimen.control_card_elevation)
+            controlAdapter = ControlAdapter(elevation)
+            setUpRecyclerView()
+        }
+
+        fun bind(model: ControlsModel) {
+            controlAdapter.changeModel(model)
+        }
+
+        private fun setUpRecyclerView() {
+            val margin = itemView.context.resources
+                .getDimensionPixelSize(R.dimen.controls_card_margin)
+            val itemDecorator = MarginItemDecorator(margin, margin)
+
+            recyclerView.apply {
+                this.adapter = controlAdapter
+                layoutManager = GridLayoutManager(recyclerView.context, 2).apply {
+                    spanSizeLookup = controlAdapter.spanSizeLookup
+                }
+                addItemDecoration(itemDecorator)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index 183dde8..a8c4851 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -340,6 +340,8 @@
 
         controlLoadCallbackCaptor.value.error("")
 
+        delayableExecutor.runAllReady()
+
         assertTrue(loaded)
     }