Changed management to just show all controls

This is the first of a series of CLs to update management screens to
latest mocks. Now ControlModel is an interface to interact with the
Adapters. Different implementers can be used to surface different views
of all the controls.

Zones are now sorted according to the order they appear on load. Zones
with no name (or blank name) are given a generic "Other" and put at the
end.

Test: atest
Test: manual
Bug: 149138395

Change-Id: Ica708d903afed582c4c6ad4a5142351cd81cec89
diff --git a/packages/SystemUI/res/layout/controls_management.xml b/packages/SystemUI/res/layout/controls_management.xml
index a7379be..6533c18 100644
--- a/packages/SystemUI/res/layout/controls_management.xml
+++ b/packages/SystemUI/res/layout/controls_management.xml
@@ -70,12 +70,14 @@
             android:layout_height="match_parent"
             android:padding="4dp">
 
-            <TextView
+            <Button
+                android:id="@+id/other_apps"
+                android:visibility="gone"
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
+                android:gravity="center_vertical"
                 android:text="See other apps"
-                android:textAppearance="@style/TextAppearance.Control.Title"
-                android:textColor="?android:attr/colorPrimary"
+                style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
                 app:layout_constraintTop_toTopOf="parent"
                 app:layout_constraintBottom_toBottomOf="parent"
                 app:layout_constraintStart_toStartOf="parent"/>
@@ -85,6 +87,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
                 android:text="Done"
+                style="@*android:style/Widget.DeviceDefault.Button.Colored"
                 app:layout_constraintTop_toTopOf="parent"
                 app:layout_constraintBottom_toBottomOf="parent"
                 app:layout_constraintEnd_toEndOf="parent"/>
diff --git a/packages/SystemUI/res/layout/controls_management_favorites.xml b/packages/SystemUI/res/layout/controls_management_favorites.xml
index 62056e6..aab32f4 100644
--- a/packages/SystemUI/res/layout/controls_management_favorites.xml
+++ b/packages/SystemUI/res/layout/controls_management_favorites.xml
@@ -14,97 +14,26 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<androidx.constraintlayout.widget.ConstraintLayout
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
 
     <TextView
-        android:id="@+id/error_message"
+        android:id="@+id/status_message"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginTop="@dimen/controls_management_list_margin"
-        android:text="@string/controls_favorite_load_error"
         android:textAppearance="?android:attr/textAppearanceSmall"
-        android:visibility="gone"
         android:gravity="center_horizontal"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toBottomOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/text_favorites"
     />
 
-    <TextView
-        android:id="@+id/text_favorites"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="@dimen/controls_management_list_margin"
-        android:text="@string/controls_favorite_header_favorites"
-        android:textAppearance="?android:attr/textAppearanceSmall"
-        android:textAllCaps="true"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/divider1"
-        app:layout_constraintTop_toBottomOf="@id/error_message"
-        />
-
-    <View
-        android:id="@+id/divider1"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/controls_app_divider_height"
-        android:layout_gravity="center_horizontal|top"
-        android:background="?android:attr/listDivider"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/listFavorites"
-        app:layout_constraintTop_toBottomOf="@id/text_favorites"
-        />
-
-    <androidx.recyclerview.widget.RecyclerView
-        android:id="@+id/listFavorites"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_marginTop="@dimen/controls_management_list_margin"
-        android:nestedScrollingEnabled="false"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/text_all"
-        app:layout_constraintTop_toBottomOf="@id/divider1"/>
-
-    <TextView
-        android:id="@+id/text_all"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="@dimen/controls_management_list_margin"
-        android:text="@string/controls_favorite_header_all"
-        android:textAppearance="?android:attr/textAppearanceSmall"
-        android:textAllCaps="true"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/divider2"
-        app:layout_constraintTop_toBottomOf="@id/listFavorites"
-        />
-
-    <View
-        android:id="@+id/divider2"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/controls_app_divider_height"
-        android:layout_gravity="center_horizontal|top"
-        android:background="?android:attr/listDivider"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/listAll"
-        app:layout_constraintTop_toBottomOf="@id/text_all"
-        />
-
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/listAll"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_marginTop="@dimen/controls_management_list_margin"
-        android:nestedScrollingEnabled="false"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/divider2"/>
+        android:nestedScrollingEnabled="false"/>
 
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 4aafec8..ff28b4d 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2614,8 +2614,8 @@
     <string name="controls_providers_subtitle">Choose an app from which to add controls</string>
     <!-- Number of favorites for controls management screen [CHAR LIMIT=NONE]-->
     <plurals name="controls_number_of_favorites">
-        <item quantity="one"><xliff:g id="number" example="1">%s</xliff:g> current favorite.</item>
-        <item quantity="other"><xliff:g id="number" example="3">%s</xliff:g> current favorites.</item>
+        <item quantity="one"><xliff:g id="number" example="1">%s</xliff:g> control added.</item>
+        <item quantity="other"><xliff:g id="number" example="3">%s</xliff:g> controls added.</item>
     </plurals>
 
     <!-- Controls management controls screen default title [CHAR LIMIT=30] -->
@@ -2626,6 +2626,10 @@
     <string name="controls_favorite_header_favorites">Favorites</string>
     <!-- Controls management controls screen all header [CHAR LIMIT=50] -->
     <string name="controls_favorite_header_all">All</string>
-    <!-- Controls management controls screen error on load message [CHAR LIMIT=50] -->
+    <!-- Controls management controls screen error on load message [CHAR LIMIT=60] -->
     <string name="controls_favorite_load_error">The list of all controls could not be loaded.</string>
+    <!-- Controls management controls screen header for Other zone [CHAR LIMIT=60] -->
+    <string name="controls_favorite_other_zone_header">Other</string>
+
+
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
new file mode 100644
index 0000000..c053517
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2019 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.text.TextUtils
+import android.util.ArrayMap
+import com.android.systemui.controls.ControlStatus
+import com.android.systemui.controls.controller.ControlInfo
+
+/**
+ * This model is used to show all 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
+ * @property noZoneString text to use as header for all controls that have blank or `null` zone.
+ */
+class AllModel(
+    private val controls: List<ControlStatus>,
+    initialFavoriteIds: List<String>,
+    private val emptyZoneString: CharSequence
+) : ControlsModel {
+
+    override val favorites: List<ControlInfo.Builder>
+        get() = favoriteIds.mapNotNull { id ->
+            val control = controls.firstOrNull { it.control.controlId == id }?.control
+            control?.let {
+                ControlInfo.Builder().apply {
+                    controlId = it.controlId
+                    controlTitle = it.title
+                    deviceType = it.deviceType
+                }
+            }
+        }
+
+    private val favoriteIds = initialFavoriteIds.toMutableList()
+
+    override val elements: List<ElementWrapper> = createWrappers(controls)
+
+    override fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
+        if (favorite) {
+            favoriteIds.add(controlId)
+        } else {
+            favoriteIds.remove(controlId)
+        }
+    }
+
+    private fun createWrappers(list: List<ControlStatus>): List<ElementWrapper> {
+        val map = list.groupByTo(OrderedMap(ArrayMap<CharSequence, MutableList<ControlStatus>>())) {
+            it.control.zone ?: ""
+        }
+        val output = mutableListOf<ElementWrapper>()
+        var emptyZoneValues: Sequence<ControlWrapper>? = null
+        for (zoneName in map.orderedKeys) {
+            val values = map.getValue(zoneName).asSequence().map { ControlWrapper(it) }
+            if (TextUtils.isEmpty(zoneName)) {
+                emptyZoneValues = values
+            } else {
+                output.add(ZoneNameWrapper(zoneName))
+                output.addAll(values)
+            }
+        }
+        // Add controls with empty zone at the end
+        if (emptyZoneValues != null) {
+            if (map.size != 1) {
+                output.add(ZoneNameWrapper(emptyZoneString))
+            }
+            output.addAll(emptyZoneValues)
+        }
+        return output
+    }
+
+    private class OrderedMap<K, V>(private val map: MutableMap<K, V>) : MutableMap<K, V> by map {
+
+        val orderedKeys = mutableListOf<K>()
+
+        override fun put(key: K, value: V): V? {
+            if (key !in map) {
+                orderedKeys.add(key)
+            }
+            return map.put(key, value)
+        }
+
+        override fun clear() {
+            orderedKeys.clear()
+            map.clear()
+        }
+
+        override fun remove(key: K): V? {
+            val removed = map.remove(key)
+            if (removed != null) {
+                orderedKeys.remove(key)
+            }
+            return removed
+        }
+    }
+}
\ No newline at end of file
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 ac5e089..25ebc65 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
@@ -116,6 +116,10 @@
 
     fun renderFavoritesForComponent(component: ComponentName): String {
         val qty = favoriteFunction(component)
-        return resources.getQuantityString(R.plurals.controls_number_of_favorites, qty, qty)
+        if (qty != 0) {
+            return resources.getQuantityString(R.plurals.controls_number_of_favorites, qty, qty)
+        } else {
+            return ""
+        }
     }
 }
\ No newline at end of file
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 d3cabe6..0870a4d 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
@@ -42,8 +42,7 @@
  * @param onlyFavorites set to true to only display favorites instead of all controls
  */
 class ControlAdapter(
-    private val layoutInflater: LayoutInflater,
-    private val onlyFavorites: Boolean = false
+    private val layoutInflater: LayoutInflater
 ) : RecyclerView.Adapter<Holder>() {
 
     companion object {
@@ -57,22 +56,21 @@
         }
     }
 
-    var modelList: List<ElementWrapper> = emptyList()
-    private var favoritesModel: FavoriteModel? = null
+    private var model: ControlsModel? = null
 
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
         return when (viewType) {
             TYPE_CONTROL -> {
                 ControlHolder(
-                        layoutInflater.inflate(R.layout.controls_base_item, parent, false).apply {
-                            layoutParams.apply {
-                                width = ViewGroup.LayoutParams.MATCH_PARENT
-                            }
-                            elevation = 15f
-                        },
-                        { id, favorite ->
-                            favoritesModel?.changeFavoriteStatus(id, favorite)
-                        })
+                    layoutInflater.inflate(R.layout.controls_base_item, parent, false).apply {
+                        layoutParams.apply {
+                            width = ViewGroup.LayoutParams.MATCH_PARENT
+                        }
+                        elevation = 15f
+                    }
+                ) { id, favorite ->
+                    model?.changeFavoriteStatus(id, favorite)
+                }
             }
             TYPE_ZONE -> {
                 ZoneHolder(layoutInflater.inflate(R.layout.controls_zone_header, parent, false))
@@ -81,27 +79,26 @@
         }
     }
 
-    fun changeFavoritesModel(favoritesModel: FavoriteModel) {
-        this.favoritesModel = favoritesModel
-        if (onlyFavorites) {
-            modelList = favoritesModel.favorites
-        } else {
-            modelList = favoritesModel.all
-        }
+    fun changeModel(model: ControlsModel) {
+        this.model = model
         notifyDataSetChanged()
     }
 
-    override fun getItemCount() = modelList.size
+    override fun getItemCount() = model?.elements?.size ?: 0
 
     override fun onBindViewHolder(holder: Holder, index: Int) {
-        holder.bindData(modelList[index])
+        model?.let {
+            holder.bindData(it.elements[index])
+        }
     }
 
     override fun getItemViewType(position: Int): Int {
-        return when (modelList[position]) {
-            is ZoneNameWrapper -> TYPE_ZONE
-            is ControlWrapper -> TYPE_CONTROL
-        }
+        model?.let {
+            return when (it.elements.get(position)) {
+                is ZoneNameWrapper -> TYPE_ZONE
+                is ControlWrapper -> TYPE_CONTROL
+            }
+        } ?: throw IllegalStateException("Getting item type for null model")
     }
 }
 
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 af4a977..2c014498 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
@@ -26,11 +26,9 @@
 import android.widget.Button
 import android.widget.TextView
 import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.RecyclerView
 import com.android.systemui.R
 import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.controls.controller.ControlInfo
 import com.android.systemui.controls.controller.ControlsControllerImpl
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.settings.CurrentUserTracker
@@ -51,33 +49,10 @@
 
     private lateinit var recyclerViewAll: RecyclerView
     private lateinit var adapterAll: ControlAdapter
-    private lateinit var recyclerViewFavorites: RecyclerView
-    private lateinit var adapterFavorites: ControlAdapter
-    private lateinit var errorText: TextView
+    private lateinit var statusText: TextView
+    private var model: ControlsModel? = null
     private var component: ComponentName? = null
 
-    private var currentModel: FavoriteModel? = null
-    private var itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(
-            /* dragDirs */ ItemTouchHelper.UP
-                    or ItemTouchHelper.DOWN
-                    or ItemTouchHelper.LEFT
-                    or ItemTouchHelper.RIGHT,
-            /* swipeDirs */0
-    ) {
-        override fun onMove(
-            recyclerView: RecyclerView,
-            viewHolder: RecyclerView.ViewHolder,
-            target: RecyclerView.ViewHolder
-        ): Boolean {
-            return currentModel?.onMoveItem(
-                    viewHolder.layoutPosition, target.layoutPosition) != null
-        }
-
-        override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
-
-        override fun isItemViewSwipeEnabled() = false
-    }
-
     private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
         private val startingUser = controller.currentUserId
 
@@ -89,6 +64,10 @@
         }
     }
 
+    override fun onBackPressed() {
+        finish()
+    }
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.controls_management)
@@ -99,21 +78,27 @@
 
         val app = intent.getCharSequenceExtra(EXTRA_APP)
         component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)
-        errorText = requireViewById(R.id.error_message)
+        statusText = requireViewById(R.id.status_message)
 
-        setUpRecyclerViews()
+        setUpRecyclerView()
 
         requireViewById<TextView>(R.id.title).text = app?.let { it }
                 ?: resources.getText(R.string.controls_favorite_default_title)
         requireViewById<TextView>(R.id.subtitle).text =
                 resources.getText(R.string.controls_favorite_subtitle)
 
+        requireViewById<Button>(R.id.other_apps).apply {
+            visibility = View.VISIBLE
+            setOnClickListener {
+                this@ControlsFavoritingActivity.onBackPressed()
+            }
+        }
+
         requireViewById<Button>(R.id.done).setOnClickListener {
             if (component == null) return@setOnClickListener
-            val favoritesForStorage = currentModel?.favorites?.map {
-                with(it.controlStatus.control) {
-                    ControlInfo(component!!, controlId, title, deviceType)
-                }
+            val favoritesForStorage = model?.favorites?.map {
+                it.componentName = component!!
+                it.build()
             }
             if (favoritesForStorage != null) {
                 controller.replaceFavoritesForComponent(component!!, favoritesForStorage)
@@ -122,20 +107,22 @@
         }
 
         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
                 executor.execute {
-                    val favoriteModel = FavoriteModel(
-                        allControls,
-                        favoriteKeys,
-                        allAdapter = adapterAll,
-                        favoritesAdapter = adapterFavorites)
-                    adapterAll.changeFavoritesModel(favoriteModel)
-                    adapterFavorites.changeFavoritesModel(favoriteModel)
-                    currentModel = favoriteModel
-                    errorText.visibility = if (error) View.VISIBLE else View.GONE
+                    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
+                    }
                 }
             })
         }
@@ -143,7 +130,7 @@
         currentUserTracker.startTracking()
     }
 
-    private fun setUpRecyclerViews() {
+    private fun setUpRecyclerView() {
         val margin = resources.getDimensionPixelSize(R.dimen.controls_card_margin)
         val itemDecorator = MarginItemDecorator(margin, margin)
         val layoutInflater = LayoutInflater.from(applicationContext)
@@ -156,14 +143,6 @@
             }
             addItemDecoration(itemDecorator)
         }
-
-        adapterFavorites = ControlAdapter(layoutInflater, true)
-        recyclerViewFavorites = requireViewById<RecyclerView>(R.id.listFavorites).apply {
-            layoutManager = GridLayoutManager(applicationContext, 2)
-            adapter = adapterFavorites
-            addItemDecoration(itemDecorator)
-        }
-        ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(recyclerViewFavorites)
     }
 
     override fun onDestroy() {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt
new file mode 100644
index 0000000..a995a2e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2019 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 com.android.systemui.controls.ControlStatus
+import com.android.systemui.controls.controller.ControlInfo
+
+/**
+ * Model for using with [ControlAdapter].
+ *
+ * Implementations of this interface provide different views of the controls to show.
+ */
+interface ControlsModel {
+
+    /**
+     * List of favorites (builders) in order.
+     *
+     * This should be obtained prior to storing the favorites using
+     * [ControlsController.replaceFavoritesForComponent].
+     */
+    val favorites: List<ControlInfo.Builder>
+
+    /**
+     * List of all the elements to display by the corresponding [RecyclerView].
+     */
+    val elements: List<ElementWrapper>
+
+    /**
+     * Change the favorite status of a particular control.
+     */
+    fun changeFavoriteStatus(controlId: String, favorite: Boolean) {}
+
+    /**
+     * Move an item (in elements) from one position to another.
+     */
+    fun onMoveItem(from: Int, to: Int) {}
+}
+
+/**
+ * Wrapper classes for the different types of elements shown in the [RecyclerView]s in
+ * [ControlAdapter].
+ */
+sealed class ElementWrapper
+data class ZoneNameWrapper(val zoneName: CharSequence) : ElementWrapper()
+data class ControlWrapper(val controlStatus: ControlStatus) : ElementWrapper()
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
index ad4bdef..098caf6 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
@@ -21,6 +21,7 @@
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.ViewStub
+import android.widget.Button
 import android.widget.TextView
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -86,6 +87,10 @@
         requireViewById<TextView>(R.id.subtitle).text =
                 resources.getText(R.string.controls_providers_subtitle)
 
+        requireViewById<Button>(R.id.done).setOnClickListener {
+            this@ControlsProviderSelectorActivity.finishAffinity()
+        }
+
         currentUserTracker.startTracking()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/FavoriteModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/FavoriteModel.kt
index 6bade0a..5c51e3d 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/FavoriteModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/FavoriteModel.kt
@@ -142,12 +142,4 @@
         else if (p0 != null && p1 == null) return 1
         return p0.toString().compareTo(p1.toString())
     }
-}
-
-/**
- * Wrapper classes for the different types of elements shown in the [RecyclerView]s in
- * [ControlsFavoritingActivity].
- */
-sealed class ElementWrapper
-data class ZoneNameWrapper(val zoneName: CharSequence) : ElementWrapper()
-data class ControlWrapper(val controlStatus: ControlStatus) : ElementWrapper()
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AllModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AllModelTest.kt
new file mode 100644
index 0000000..68e1ec1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AllModelTest.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2019 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.app.PendingIntent
+import android.service.controls.Control
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlStatus
+import com.android.systemui.controls.controller.ControlInfo
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class AllModelTest : SysuiTestCase() {
+
+    companion object {
+        private const val EMPTY_STRING = "Other"
+    }
+
+    @Mock
+    lateinit var pendingIntent: PendingIntent
+
+    val idPrefix = "controlId"
+    val favoritesIndices = listOf(7, 3, 1, 9)
+    val favoritesList = favoritesIndices.map { "controlId$it" }
+    lateinit var controls: List<ControlStatus>
+
+    lateinit var model: AllModel
+
+    private fun zoneMap(id: Int): String? {
+        return when (id) {
+            10 -> ""
+            11 -> null
+            else -> ((id + 1) % 3).toString()
+        }
+    }
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        // controlId0 --> zone = 1
+        // controlId1 --> zone = 2, favorite
+        // controlId2 --> zone = 0
+        // controlId3 --> zone = 1, favorite
+        // controlId4 --> zone = 2
+        // controlId5 --> zone = 0
+        // controlId6 --> zone = 1
+        // controlId7 --> zone = 2, favorite
+        // controlId8 --> zone = 0
+        // controlId9 --> zone = 1, favorite
+        // controlId10 --> zone = ""
+        // controlId11 --> zone = null
+        controls = (0..11).map {
+            ControlStatus(
+                    Control.StatelessBuilder("$idPrefix$it", pendingIntent)
+                            .setZone(zoneMap(it))
+                            .build(),
+                    it in favoritesIndices
+            )
+        }
+        model = AllModel(controls, favoritesList, EMPTY_STRING)
+    }
+
+    @Test
+    fun testElements() {
+
+        // Zones are sorted by order of appearance, with empty at the end with special header.
+        val expected = listOf(
+                ZoneNameWrapper("1"),
+                ControlWrapper(controls[0]),
+                ControlWrapper(controls[3]),
+                ControlWrapper(controls[6]),
+                ControlWrapper(controls[9]),
+                ZoneNameWrapper("2"),
+                ControlWrapper(controls[1]),
+                ControlWrapper(controls[4]),
+                ControlWrapper(controls[7]),
+                ZoneNameWrapper("0"),
+                ControlWrapper(controls[2]),
+                ControlWrapper(controls[5]),
+                ControlWrapper(controls[8]),
+                ZoneNameWrapper(EMPTY_STRING),
+                ControlWrapper(controls[10]),
+                ControlWrapper(controls[11])
+        )
+        expected.zip(model.elements).forEachIndexed { index, it ->
+            assertEquals("Error in item at index $index", it.first, it.second)
+        }
+    }
+
+    private fun sameControl(controlInfo: ControlInfo.Builder, control: Control): Boolean {
+        return controlInfo.controlId == control.controlId &&
+                controlInfo.controlTitle == control.title &&
+                controlInfo.deviceType == control.deviceType
+    }
+
+    @Test
+    fun testAllEmpty_noHeader() {
+        val selected_controls = listOf(controls[10], controls[11])
+        val new_model = AllModel(selected_controls, emptyList(), EMPTY_STRING)
+        val expected = listOf(
+                ControlWrapper(controls[10]),
+                ControlWrapper(controls[11])
+        )
+
+        expected.zip(new_model.elements).forEachIndexed { index, it ->
+            assertEquals("Error in item at index $index", it.first, it.second)
+        }
+    }
+
+    @Test
+    fun testFavorites() {
+        val expectedFavorites = favoritesIndices.map(controls::get).map(ControlStatus::control)
+        model.favorites.zip(expectedFavorites).forEach {
+            assertTrue(sameControl(it.first, it.second))
+        }
+    }
+
+    @Test
+    fun testAddFavorite() {
+        val indexToAdd = 6
+        model.changeFavoriteStatus("$idPrefix$indexToAdd", true)
+
+        val expectedFavorites = favoritesIndices.map(controls::get).map(ControlStatus::control) +
+                controls[indexToAdd].control
+
+        model.favorites.zip(expectedFavorites).forEach {
+            assertTrue(sameControl(it.first, it.second))
+        }
+    }
+
+    @Test
+    fun testAddFavorite_alreadyThere() {
+        val indexToAdd = 7
+        model.changeFavoriteStatus("$idPrefix$indexToAdd", true)
+
+        val expectedFavorites = favoritesIndices.map(controls::get).map(ControlStatus::control)
+
+        model.favorites.zip(expectedFavorites).forEach {
+            assertTrue(sameControl(it.first, it.second))
+        }
+    }
+
+    @Test
+    fun testRemoveFavorite() {
+        val indexToRemove = 3
+        model.changeFavoriteStatus("$idPrefix$indexToRemove", false)
+
+        val expectedFavorites = (favoritesIndices.filterNot { it == indexToRemove })
+                .map(controls::get)
+                .map(ControlStatus::control)
+
+        model.favorites.zip(expectedFavorites).forEach {
+            assertTrue(sameControl(it.first, it.second))
+        }
+    }
+
+    @Test
+    fun testRemoveFavorite_notThere() {
+        val indexToRemove = 4
+        model.changeFavoriteStatus("$idPrefix$indexToRemove", false)
+
+        val expectedFavorites = favoritesIndices.map(controls::get).map(ControlStatus::control)
+
+        model.favorites.zip(expectedFavorites).forEach {
+            assertTrue(sameControl(it.first, it.second))
+        }
+    }
+}
\ No newline at end of file