summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/AndroidManifest.xml9
-rw-r--r--packages/SystemUI/res/layout/controls_horizontal_divider_withEmpty.xml44
-rw-r--r--packages/SystemUI/res/layout/controls_management_apps.xml17
-rw-r--r--packages/SystemUI/res/layout/controls_management_editing.xml27
-rw-r--r--packages/SystemUI/res/values/strings.xml7
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt74
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt176
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/FavoriteModel.kt145
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt221
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt41
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt32
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/controls/management/AllModelTest.kt59
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoriteModelTest.kt200
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoritesModelTest.kt291
22 files changed, 1077 insertions, 443 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index f767636f7c55..133d375b8c6e 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -679,6 +679,15 @@
android:visibleToInstantApps="true">
</activity>
+ <activity android:name=".controls.management.ControlsEditingActivity"
+ android:theme="@style/Theme.ControlsManagement"
+ android:excludeFromRecents="true"
+ android:showForAllUsers="true"
+ android:finishOnTaskLaunch="true"
+ android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
+ android:visibleToInstantApps="true">
+ </activity>
+
<activity android:name=".controls.management.ControlsFavoritingActivity"
android:theme="@style/Theme.ControlsManagement"
android:excludeFromRecents="true"
diff --git a/packages/SystemUI/res/layout/controls_horizontal_divider_withEmpty.xml b/packages/SystemUI/res/layout/controls_horizontal_divider_withEmpty.xml
new file mode 100644
index 000000000000..90b3398e3de2
--- /dev/null
+++ b/packages/SystemUI/res/layout/controls_horizontal_divider_withEmpty.xml
@@ -0,0 +1,44 @@
+<?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.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/controls_management_list_margin"
+ />
+
+ <FrameLayout
+ android:id="@+id/frame"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/control_height"
+ android:visibility="gone"
+ >
+ </FrameLayout>
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginBottom="10dp"
+ android:layout_marginStart="40dp"
+ android:layout_marginEnd="40dp"
+ android:background="#4dffffff" />
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/controls_management_apps.xml b/packages/SystemUI/res/layout/controls_management_apps.xml
index 42d73f3cc9ce..94df9d8f4775 100644
--- a/packages/SystemUI/res/layout/controls_management_apps.xml
+++ b/packages/SystemUI/res/layout/controls_management_apps.xml
@@ -14,18 +14,11 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<androidx.core.widget.NestedScrollView
+<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/list"
android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:orientation="vertical"
- android:layout_marginTop="@dimen/controls_management_list_margin">
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/controls_management_list_margin"
+/>
- <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_editing.xml b/packages/SystemUI/res/layout/controls_management_editing.xml
new file mode 100644
index 000000000000..8a14ec3666b2
--- /dev/null
+++ b/packages/SystemUI/res/layout/controls_management_editing.xml
@@ -0,0 +1,27 @@
+<?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.recyclerview.widget.RecyclerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:paddingTop="@dimen/controls_management_list_margin"
+/>
+
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 566d143208fc..7c0b6054dddb 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2670,8 +2670,11 @@
<string name="controls_favorite_default_title">Controls</string>
<!-- Controls management controls screen subtitle [CHAR LIMIT=NONE] -->
<string name="controls_favorite_subtitle">Choose controls to access from the power menu</string>
- <!-- Controls management controls screen, user direction for rearranging controls [CHAR LIMIT=NONE] -->
- <string name="controls_favorite_rearrange">Hold and drag a control to move it</string>
+ <!-- Controls management editing screen, user direction for rearranging controls [CHAR LIMIT=NONE] -->
+ <string name="controls_favorite_rearrange">Hold &amp; drag to rearrange controls</string>
+
+ <!-- Controls management editing screen, text to indicate that all the favorites have been removed [CHAR LIMIT=NONE] -->
+ <string name="controls_favorite_removed">All controls removed</string>
<!-- 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>
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt
index dec60073a55e..5891a7f705c8 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt
@@ -18,10 +18,34 @@ package com.android.systemui.controls
import android.content.ComponentName
import android.service.controls.Control
+import android.service.controls.DeviceTypes
+
+interface ControlInterface {
+ val favorite: Boolean
+ val component: ComponentName
+ val controlId: String
+ val title: CharSequence
+ val subtitle: CharSequence
+ val removed: Boolean
+ get() = false
+ @DeviceTypes.DeviceType val deviceType: Int
+}
data class ControlStatus(
val control: Control,
- val component: ComponentName,
- var favorite: Boolean,
- val removed: Boolean = false
-)
+ override val component: ComponentName,
+ override var favorite: Boolean,
+ override val removed: Boolean = false
+) : ControlInterface {
+ override val controlId: String
+ get() = control.controlId
+
+ override val title: CharSequence
+ get() = control.title
+
+ override val subtitle: CharSequence
+ get() = control.subtitle
+
+ @DeviceTypes.DeviceType override val deviceType: Int
+ get() = control.deviceType
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt
index 6e59ac162657..40606c2689e5 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt
@@ -16,6 +16,7 @@
package com.android.systemui.controls.controller
+import android.service.controls.Control
import android.service.controls.DeviceTypes
/**
@@ -39,6 +40,14 @@ data class ControlInfo(
companion object {
private const val SEPARATOR = ":"
+ fun fromControl(control: Control): ControlInfo {
+ return ControlInfo(
+ control.controlId,
+ control.title,
+ control.subtitle,
+ control.deviceType
+ )
+ }
}
/**
@@ -49,13 +58,4 @@ data class ControlInfo(
override fun toString(): String {
return "$SEPARATOR$controlId$SEPARATOR$controlTitle$SEPARATOR$deviceType"
}
-
- class Builder {
- lateinit var controlId: String
- lateinit var controlTitle: CharSequence
- lateinit var controlSubtitle: CharSequence
- var deviceType: Int = DeviceTypes.TYPE_UNKNOWN
-
- fun build() = ControlInfo(controlId, controlTitle, controlSubtitle, deviceType)
- }
}
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 568fb289027d..7cab847d52f7 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
@@ -148,6 +148,18 @@ interface ControlsController : UserAwareController {
fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo>
/**
+ * Get all the favorites for a given structure.
+ *
+ * @param componentName the name of the service that provides the [Control]
+ * @param structureName the name of the structure
+ * @return a list of the current favorites in that structure
+ */
+ fun getFavoritesForStructure(
+ componentName: ComponentName,
+ structureName: CharSequence
+ ): List<ControlInfo>
+
+ /**
* Adds a single favorite to a given component and structure
* @param componentName the name of the service that provides the [Control]
* @param structureName the name of the structure that holds the [Control]
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 8805694616a4..6d34009169d5 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -544,6 +544,15 @@ class ControlsControllerImpl @Inject constructor (
override fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo> =
Favorites.getStructuresForComponent(componentName)
+ override fun getFavoritesForStructure(
+ componentName: ComponentName,
+ structureName: CharSequence
+ ): List<ControlInfo> {
+ return Favorites.getControlsForStructure(
+ StructureInfo(componentName, structureName, emptyList())
+ )
+ }
+
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
pw.println("ControlsController state:")
pw.println(" Available: $available")
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
index 946a2365585a..3bed55912332 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
@@ -22,6 +22,7 @@ import com.android.systemui.controls.controller.ControlsBindingControllerImpl
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.controller.ControlsControllerImpl
import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper
+import com.android.systemui.controls.management.ControlsEditingActivity
import com.android.systemui.controls.management.ControlsFavoritingActivity
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.management.ControlsListingControllerImpl
@@ -73,6 +74,13 @@ abstract class ControlsModule {
@Binds
@IntoMap
+ @ClassKey(ControlsEditingActivity::class)
+ abstract fun provideControlsEditingActivity(
+ activity: ControlsEditingActivity
+ ): Activity
+
+ @Binds
+ @IntoMap
@ClassKey(ControlsRequestDialog::class)
abstract fun provideControlsRequestDialog(
activity: ControlsRequestDialog
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 11181e56838e..175ed061c714 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
@@ -37,23 +37,22 @@ import com.android.systemui.controls.controller.ControlInfo
* @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.
+ * @property controlsModelCallback callback to notify that favorites have changed for the first time
*/
class AllModel(
private val controls: List<ControlStatus>,
initialFavoriteIds: List<String>,
- private val emptyZoneString: CharSequence
+ private val emptyZoneString: CharSequence,
+ private val controlsModelCallback: ControlsModel.ControlsModelCallback
) : ControlsModel {
- override val favorites: List<ControlInfo.Builder>
+ private var modified = false
+
+ override val favorites: List<ControlInfo>
get() = favoriteIds.mapNotNull { id ->
val control = controls.firstOrNull { it.control.controlId == id }?.control
control?.let {
- ControlInfo.Builder().apply {
- controlId = it.controlId
- controlTitle = it.title
- controlSubtitle = it.subtitle
- deviceType = it.deviceType
- }
+ ControlInfo.fromControl(it)
}
}
@@ -66,14 +65,18 @@ class AllModel(
override fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
val toChange = elements.firstOrNull {
- it is ControlWrapper && it.controlStatus.control.controlId == controlId
- } as ControlWrapper?
+ it is ControlStatusWrapper && it.controlStatus.control.controlId == controlId
+ } as ControlStatusWrapper?
if (favorite == toChange?.controlStatus?.favorite) return
- if (favorite) {
+ val changed: Boolean = if (favorite) {
favoriteIds.add(controlId)
} else {
favoriteIds.remove(controlId)
}
+ if (changed && !modified) {
+ modified = true
+ controlsModelCallback.onFirstChange()
+ }
toChange?.let {
it.controlStatus.favorite = favorite
}
@@ -84,9 +87,9 @@ class AllModel(
it.control.zone ?: ""
}
val output = mutableListOf<ElementWrapper>()
- var emptyZoneValues: Sequence<ControlWrapper>? = null
+ var emptyZoneValues: Sequence<ControlStatusWrapper>? = null
for (zoneName in map.orderedKeys) {
- val values = map.getValue(zoneName).asSequence().map { ControlWrapper(it) }
+ val values = map.getValue(zoneName).asSequence().map { ControlStatusWrapper(it) }
if (TextUtils.isEmpty(zoneName)) {
emptyZoneValues = values
} else {
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 1291dd98932e..607934c3bae7 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
@@ -28,6 +28,7 @@ import android.widget.TextView
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R
+import com.android.systemui.controls.ControlInterface
import com.android.systemui.controls.ui.RenderInfo
private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
@@ -35,11 +36,10 @@ private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
/**
* Adapter for binding [Control] information to views.
*
- * The model for this adapter is provided by a [FavoriteModel] that is set using
+ * The model for this adapter is provided by a [ControlModel] that is set using
* [changeFavoritesModel]. This allows for updating the model if there's a reload.
*
- * @param layoutInflater an inflater for the views in the containing [RecyclerView]
- * @param onlyFavorites set to true to only display favorites instead of all controls
+ * @property elevation elevation of each control view
*/
class ControlAdapter(
private val elevation: Float
@@ -48,11 +48,12 @@ class ControlAdapter(
companion object {
private const val TYPE_ZONE = 0
private const val TYPE_CONTROL = 1
+ private const val TYPE_DIVIDER = 2
}
val spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
- return if (getItemViewType(position) == TYPE_ZONE) 2 else 1
+ return if (getItemViewType(position) != TYPE_CONTROL) 2 else 1
}
}
@@ -78,6 +79,10 @@ class ControlAdapter(
TYPE_ZONE -> {
ZoneHolder(layoutInflater.inflate(R.layout.controls_zone_header, parent, false))
}
+ TYPE_DIVIDER -> {
+ DividerHolder(layoutInflater.inflate(
+ R.layout.controls_horizontal_divider_withEmpty, parent, false))
+ }
else -> throw IllegalStateException("Wrong viewType: $viewType")
}
}
@@ -95,11 +100,26 @@ class ControlAdapter(
}
}
+ override fun onBindViewHolder(holder: Holder, position: Int, payloads: MutableList<Any>) {
+ if (payloads.isEmpty()) {
+ super.onBindViewHolder(holder, position, payloads)
+ } else {
+ model?.let {
+ val el = it.elements[position]
+ if (el is ControlInterface) {
+ holder.updateFavorite(el.favorite)
+ }
+ }
+ }
+ }
+
override fun getItemViewType(position: Int): Int {
model?.let {
return when (it.elements.get(position)) {
is ZoneNameWrapper -> TYPE_ZONE
- is ControlWrapper -> TYPE_CONTROL
+ is ControlStatusWrapper -> TYPE_CONTROL
+ is ControlInfoWrapper -> TYPE_CONTROL
+ is DividerWrapper -> TYPE_DIVIDER
}
} ?: throw IllegalStateException("Getting item type for null model")
}
@@ -115,6 +135,24 @@ sealed class Holder(view: View) : RecyclerView.ViewHolder(view) {
* Bind the data from the model into the view
*/
abstract fun bindData(wrapper: ElementWrapper)
+
+ open fun updateFavorite(favorite: Boolean) {}
+}
+
+/**
+ * Holder for using with [DividerWrapper] to display a divider between zones.
+ *
+ * The divider can be shown or hidden. It also has a frame view the height of a control, that can
+ * be toggled visible or gone.
+ */
+private class DividerHolder(view: View) : Holder(view) {
+ private val frame: View = itemView.requireViewById(R.id.frame)
+ private val divider: View = itemView.requireViewById(R.id.divider)
+ override fun bindData(wrapper: ElementWrapper) {
+ wrapper as DividerWrapper
+ frame.visibility = if (wrapper.showNone) View.VISIBLE else View.GONE
+ divider.visibility = if (wrapper.showDivider) View.VISIBLE else View.GONE
+ }
}
/**
@@ -130,11 +168,14 @@ private class ZoneHolder(view: View) : Holder(view) {
}
/**
- * Holder for using with [ControlWrapper] to display names of zones.
+ * Holder for using with [ControlStatusWrapper] to display names of zones.
* @param favoriteCallback this callback will be called whenever the favorite state of the
* [Control] this view represents changes.
*/
-private class ControlHolder(view: View, val favoriteCallback: ModelFavoriteChanger) : Holder(view) {
+internal class ControlHolder(
+ view: View,
+ val favoriteCallback: ModelFavoriteChanger
+) : Holder(view) {
private val icon: ImageView = itemView.requireViewById(R.id.icon)
private val title: TextView = itemView.requireViewById(R.id.title)
private val subtitle: TextView = itemView.requireViewById(R.id.subtitle)
@@ -144,20 +185,23 @@ private class ControlHolder(view: View, val favoriteCallback: ModelFavoriteChang
}
override fun bindData(wrapper: ElementWrapper) {
- wrapper as ControlWrapper
- val data = wrapper.controlStatus
- val renderInfo = getRenderInfo(data.component, data.control.deviceType)
- title.text = data.control.title
- subtitle.text = data.control.subtitle
- favorite.isChecked = data.favorite
- removed.text = if (data.removed) "Removed" else ""
+ wrapper as ControlInterface
+ val renderInfo = getRenderInfo(wrapper.component, wrapper.deviceType)
+ title.text = wrapper.title
+ subtitle.text = wrapper.subtitle
+ favorite.isChecked = wrapper.favorite
+ removed.text = if (wrapper.removed) "Removed" else ""
itemView.setOnClickListener {
favorite.isChecked = !favorite.isChecked
- favoriteCallback(data.control.controlId, favorite.isChecked)
+ favoriteCallback(wrapper.controlId, favorite.isChecked)
}
applyRenderInfo(renderInfo)
}
+ override fun updateFavorite(favorite: Boolean) {
+ this.favorite.isChecked = favorite
+ }
+
private fun getRenderInfo(
component: ComponentName,
@DeviceTypes.DeviceType deviceType: Int
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
new file mode 100644
index 000000000000..ee1ce7ab3d83
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
@@ -0,0 +1,176 @@
+/*
+ * 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.app.Activity
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.view.ViewStub
+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.ControlsControllerImpl
+import com.android.systemui.controls.controller.StructureInfo
+import com.android.systemui.settings.CurrentUserTracker
+import javax.inject.Inject
+
+/**
+ * Activity for rearranging and removing controls for a given structure
+ */
+class ControlsEditingActivity @Inject constructor(
+ private val controller: ControlsControllerImpl,
+ broadcastDispatcher: BroadcastDispatcher
+) : Activity() {
+
+ companion object {
+ private const val TAG = "ControlsEditingActivity"
+ private const val EXTRA_STRUCTURE = ControlsFavoritingActivity.EXTRA_STRUCTURE
+ private val SUBTITLE_ID = R.string.controls_favorite_rearrange
+ private val EMPTY_TEXT_ID = R.string.controls_favorite_removed
+ }
+
+ private lateinit var component: ComponentName
+ private lateinit var structure: CharSequence
+ private lateinit var model: FavoritesModel
+ private lateinit var subtitle: TextView
+ private lateinit var saveButton: View
+
+ private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
+ private val startingUser = controller.currentUserId
+
+ override fun onUserSwitched(newUserId: Int) {
+ if (newUserId != startingUser) {
+ stopTracking()
+ finish()
+ }
+ }
+ }
+
+ override fun onBackPressed() {
+ finish()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)?.let {
+ component = it
+ } ?: run(this::finish)
+
+ intent.getCharSequenceExtra(EXTRA_STRUCTURE)?.let {
+ structure = it
+ } ?: run(this::finish)
+
+ bindViews()
+
+ bindButtons()
+
+ setUpList()
+
+ currentUserTracker.startTracking()
+ }
+
+ private fun bindViews() {
+ setContentView(R.layout.controls_management)
+ requireViewById<ViewStub>(R.id.stub).apply {
+ layoutResource = R.layout.controls_management_editing
+ inflate()
+ }
+ requireViewById<TextView>(R.id.title).text = structure
+ subtitle = requireViewById<TextView>(R.id.subtitle).apply {
+ setText(SUBTITLE_ID)
+ }
+ }
+
+ private fun bindButtons() {
+ requireViewById<Button>(R.id.other_apps).apply {
+ visibility = View.VISIBLE
+ setText(R.string.controls_menu_add)
+ setOnClickListener {
+ saveFavorites()
+ val intent = Intent(this@ControlsEditingActivity,
+ ControlsFavoritingActivity::class.java).apply {
+ putExtras(this@ControlsEditingActivity.intent)
+ putExtra(ControlsFavoritingActivity.EXTRA_SINGLE_STRUCTURE, true)
+ }
+ startActivity(intent)
+ finish()
+ }
+ }
+
+ saveButton = requireViewById<Button>(R.id.done).apply {
+ isEnabled = false
+ setText(R.string.save)
+ setOnClickListener {
+ saveFavorites()
+ finishAffinity()
+ }
+ }
+ }
+
+ private fun saveFavorites() {
+ controller.replaceFavoritesForStructure(
+ StructureInfo(component, structure, model.favorites))
+ }
+
+ private val favoritesModelCallback = object : FavoritesModel.FavoritesModelCallback {
+ override fun onNoneChanged(showNoFavorites: Boolean) {
+ if (showNoFavorites) {
+ subtitle.setText(EMPTY_TEXT_ID)
+ } else {
+ subtitle.setText(SUBTITLE_ID)
+ }
+ }
+
+ override fun onFirstChange() {
+ saveButton.isEnabled = true
+ }
+ }
+
+ private fun setUpList() {
+ val controls = controller.getFavoritesForStructure(component, structure)
+ model = FavoritesModel(component, controls, favoritesModelCallback)
+ val elevation = resources.getFloat(R.dimen.control_card_elevation)
+ val adapter = ControlAdapter(elevation)
+ val recycler = requireViewById<RecyclerView>(R.id.list)
+ val margin = resources
+ .getDimensionPixelSize(R.dimen.controls_card_margin)
+ val itemDecorator = MarginItemDecorator(margin, margin)
+
+ recycler.apply {
+ this.adapter = adapter
+ layoutManager = GridLayoutManager(recycler.context, 2).apply {
+ spanSizeLookup = adapter.spanSizeLookup
+ }
+ addItemDecoration(itemDecorator)
+ }
+ adapter.changeModel(model)
+ model.attachAdapter(adapter)
+ ItemTouchHelper(model.itemTouchHelperCallback).attachToRecyclerView(recycler)
+ }
+
+ override fun onDestroy() {
+ currentUserTracker.stopTracking()
+ super.onDestroy()
+ }
+} \ No newline at end of file
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 070c4f36b39a..6f34deeb8547 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
@@ -61,6 +61,7 @@ class ControlsFavoritingActivity @Inject constructor(
// If provided, show this structure page first
const val EXTRA_STRUCTURE = "extra_structure"
+ const val EXTRA_SINGLE_STRUCTURE = "extra_single_structure"
private const val TOOLTIP_PREFS_KEY = Prefs.Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT
private const val TOOLTIP_MAX_SHOWN = 2
}
@@ -131,6 +132,12 @@ class ControlsFavoritingActivity @Inject constructor(
currentUserTracker.startTracking()
}
+ private val controlsModelCallback = object : ControlsModel.ControlsModelCallback {
+ override fun onFirstChange() {
+ doneButton.isEnabled = true
+ }
+ }
+
private fun loadControls() {
component?.let {
statusText.text = resources.getText(com.android.internal.R.string.loading)
@@ -142,15 +149,20 @@ class ControlsFavoritingActivity @Inject constructor(
val error = data.errorOnLoad
val controlsByStructure = allControls.groupBy { it.control.structure ?: "" }
listOfStructures = controlsByStructure.map {
- StructureContainer(it.key, AllModel(it.value, favoriteKeys, emptyZoneString))
+ StructureContainer(it.key, AllModel(
+ it.value, favoriteKeys, emptyZoneString, controlsModelCallback))
}.sortedWith(comparator)
val structureIndex = listOfStructures.indexOfFirst {
sc -> sc.structureName == structureExtra
}.let { if (it == -1) 0 else it }
+ // If we were requested to show a single structure, set the list to just that one
+ if (intent.getBooleanExtra(EXTRA_SINGLE_STRUCTURE, false)) {
+ listOfStructures = listOf(listOfStructures[structureIndex])
+ }
+
executor.execute {
- doneButton.isEnabled = true
structurePager.adapter = StructureAdapter(listOfStructures)
structurePager.setCurrentItem(structureIndex)
if (error) {
@@ -275,7 +287,7 @@ class ControlsFavoritingActivity @Inject constructor(
setOnClickListener {
if (component == null) return@setOnClickListener
listOfStructures.forEach {
- val favoritesForStorage = it.model.favorites.map { it.build() }
+ val favoritesForStorage = it.model.favorites
controller.replaceFavoritesForStructure(
StructureInfo(component!!, it.structureName, favoritesForStorage)
)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt
index a995a2ebfd25..37b6d15c0afe 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt
@@ -16,6 +16,9 @@
package com.android.systemui.controls.management
+import android.content.ComponentName
+import androidx.recyclerview.widget.RecyclerView
+import com.android.systemui.controls.ControlInterface
import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.controller.ControlInfo
@@ -27,12 +30,12 @@ import com.android.systemui.controls.controller.ControlInfo
interface ControlsModel {
/**
- * List of favorites (builders) in order.
+ * List of favorites in order.
*
* This should be obtained prior to storing the favorites using
* [ControlsController.replaceFavoritesForComponent].
*/
- val favorites: List<ControlInfo.Builder>
+ val favorites: List<ControlInfo>
/**
* List of all the elements to display by the corresponding [RecyclerView].
@@ -48,6 +51,24 @@ interface ControlsModel {
* Move an item (in elements) from one position to another.
*/
fun onMoveItem(from: Int, to: Int) {}
+
+ /**
+ * Attach an adapter to the model.
+ *
+ * This can be used to notify the adapter of changes in the model.
+ */
+ fun attachAdapter(adapter: RecyclerView.Adapter<*>) {}
+
+ /**
+ * Callback to notify elements (other than the adapter) of relevant changes in the model.
+ */
+ interface ControlsModelCallback {
+
+ /**
+ * Use to notify that the model has changed for the first time
+ */
+ fun onFirstChange()
+ }
}
/**
@@ -55,5 +76,29 @@ interface ControlsModel {
* [ControlAdapter].
*/
sealed class ElementWrapper
+
data class ZoneNameWrapper(val zoneName: CharSequence) : ElementWrapper()
-data class ControlWrapper(val controlStatus: ControlStatus) : ElementWrapper() \ No newline at end of file
+
+data class ControlStatusWrapper(
+ val controlStatus: ControlStatus
+) : ElementWrapper(), ControlInterface by controlStatus
+
+data class ControlInfoWrapper(
+ override val component: ComponentName,
+ val controlInfo: ControlInfo,
+ override var favorite: Boolean
+) : ElementWrapper(), ControlInterface {
+ override val controlId: String
+ get() = controlInfo.controlId
+ override val title: CharSequence
+ get() = controlInfo.controlTitle
+ override val subtitle: CharSequence
+ get() = controlInfo.controlSubtitle
+ override val deviceType: Int
+ get() = controlInfo.deviceType
+}
+
+data class DividerWrapper(
+ var showNone: Boolean = false,
+ var showDivider: Boolean = false
+) : ElementWrapper() \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/FavoriteModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/FavoriteModel.kt
deleted file mode 100644
index 5c51e3dbe4ac..000000000000
--- a/packages/SystemUI/src/com/android/systemui/controls/management/FavoriteModel.kt
+++ /dev/null
@@ -1,145 +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.controls.management
-
-import android.text.TextUtils
-import android.util.Log
-import com.android.systemui.controls.ControlStatus
-import java.util.Collections
-import java.util.Comparator
-
-/**
- * Model for keeping track of current favorites and their order.
- *
- * This model is to be used with two [ControlAdapter] one that shows only favorites in the current
- * order and another that shows all controls, separated by zone. When the favorite state of any
- * control is modified or when the favorites are reordered, the adapters are notified of the change.
- *
- * @param listControls list of all the [ControlStatus] to display. This includes controls currently
- * marked as favorites as well as those that have been removed (not returned
- * from load)
- * @param listFavoritesIds list of the [Control.controlId] for all the favorites, including those
- * that have been removed.
- * @param favoritesAdapter [ControlAdapter] used by the [RecyclerView] that shows only favorites
- * @param allAdapter [ControlAdapter] used by the [RecyclerView] that shows all controls
- */
-class FavoriteModel(
- private val listControls: List<ControlStatus>,
- listFavoritesIds: List<String>,
- private val favoritesAdapter: ControlAdapter,
- private val allAdapter: ControlAdapter
-) {
-
- companion object {
- private const val TAG = "FavoriteModel"
- }
-
- /**
- * List of favorite controls ([ControlWrapper]) in order.
- *
- * Initially, this list will give a list of wrappers in the order specified by the constructor
- * variable `listFavoriteIds`.
- *
- * As the favorites are added, removed or moved, this list will keep track of those changes.
- */
- val favorites: List<ControlWrapper> = listFavoritesIds.map { id ->
- ControlWrapper(listControls.first { it.control.controlId == id })
- }.toMutableList()
-
- /**
- * List of all controls by zones.
- *
- * Lists all the controls with the zone names interleaved as a flat list. After each zone name,
- * the controls in that zone are listed. Zones are listed in alphabetical order
- */
- val all: List<ElementWrapper> = listControls.groupBy { it.control.zone }
- .mapKeys { it.key ?: "" } // map null to empty
- .toSortedMap(CharSequenceComparator())
- .flatMap {
- val controls = it.value.map { ControlWrapper(it) }
- if (!TextUtils.isEmpty(it.key)) {
- listOf(ZoneNameWrapper(it.key)) + controls
- } else {
- controls
- }
- }
-
- /**
- * Change the favorite status of a [Control].
- *
- * This can be invoked from any of the [ControlAdapter]. It will change the status of that
- * control and either add it to the list of favorites (at the end) or remove it from it.
- *
- * Removing the favorite status from a Removed control will make it disappear completely if
- * changes are saved.
- *
- * @param controlId the id of the [Control] to change the status
- * @param favorite `true` if and only if it's set to be a favorite.
- */
- fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
- favorites as MutableList
- val index = all.indexOfFirst {
- it is ControlWrapper && it.controlStatus.control.controlId == controlId
- }
- val control = (all[index] as ControlWrapper).controlStatus
- if (control.favorite == favorite) {
- Log.d(TAG, "Changing favorite to same state for ${control.control.controlId} ")
- return
- } else {
- control.favorite = favorite
- }
- allAdapter.notifyItemChanged(index)
- if (favorite) {
- favorites.add(all[index] as ControlWrapper)
- favoritesAdapter.notifyItemInserted(favorites.size - 1)
- } else {
- val i = favorites.indexOfFirst { it.controlStatus.control.controlId == controlId }
- favorites.removeAt(i)
- favoritesAdapter.notifyItemRemoved(i)
- }
- }
-
- /**
- * Move items in the model and notify the [favoritesAdapter].
- */
- fun onMoveItem(from: Int, to: Int) {
- if (from < to) {
- for (i in from until to) {
- Collections.swap(favorites, i, i + 1)
- }
- } else {
- for (i in from downTo to + 1) {
- Collections.swap(favorites, i, i - 1)
- }
- }
- favoritesAdapter.notifyItemMoved(from, to)
- }
-}
-
-/**
- * Compares [CharSequence] as [String].
- *
- * It will have empty strings as the first element
- */
-class CharSequenceComparator : Comparator<CharSequence> {
- override fun compare(p0: CharSequence?, p1: CharSequence?): Int {
- if (p0 == null && p1 == null) return 0
- else if (p0 == null && p1 != null) return -1
- else if (p0 != null && p1 == null) return 1
- return p0.toString().compareTo(p1.toString())
- }
-} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt
new file mode 100644
index 000000000000..411170cb322c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt
@@ -0,0 +1,221 @@
+/*
+ * 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.ComponentName
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.RecyclerView
+import com.android.systemui.controls.ControlInterface
+import com.android.systemui.controls.controller.ControlInfo
+import java.util.Collections
+
+/**
+ * Model used to show and rearrange favorites.
+ *
+ * The model will show all the favorite controls and a divider that can be toggled visible/gone.
+ * It will place the items selected as favorites before the divider and the ones unselected after.
+ *
+ * @property componentName used by the [ControlAdapter] to retrieve resources.
+ * @property favorites list of current favorites
+ * @property favoritesModelCallback callback to notify on first change and empty favorites
+ */
+class FavoritesModel(
+ private val componentName: ComponentName,
+ favorites: List<ControlInfo>,
+ private val favoritesModelCallback: FavoritesModelCallback
+) : ControlsModel {
+
+ private var adapter: RecyclerView.Adapter<*>? = null
+ private var modified = false
+
+ override fun attachAdapter(adapter: RecyclerView.Adapter<*>) {
+ this.adapter = adapter
+ }
+
+ override val favorites: List<ControlInfo>
+ get() = elements.take(dividerPosition).map {
+ (it as ControlInfoWrapper).controlInfo
+ }
+
+ override val elements: List<ElementWrapper> = favorites.map {
+ ControlInfoWrapper(componentName, it, true)
+ } + DividerWrapper()
+
+ /**
+ * Indicates the position of the divider to determine
+ */
+ private var dividerPosition = elements.size - 1
+
+ override fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
+ val position = elements.indexOfFirst { it is ControlInterface && it.controlId == controlId }
+ if (position == -1) {
+ return // controlId not found
+ }
+ if (position < dividerPosition && favorite || position > dividerPosition && !favorite) {
+ return // Does not change favorite status
+ }
+ if (favorite) {
+ onMoveItemInternal(position, dividerPosition)
+ } else {
+ onMoveItemInternal(position, elements.size - 1)
+ }
+ }
+
+ override fun onMoveItem(from: Int, to: Int) {
+ onMoveItemInternal(from, to)
+ }
+
+ private fun updateDividerNone(oldDividerPosition: Int, show: Boolean) {
+ (elements[oldDividerPosition] as DividerWrapper).showNone = show
+ favoritesModelCallback.onNoneChanged(show)
+ }
+
+ private fun updateDividerShow(oldDividerPosition: Int, show: Boolean) {
+ (elements[oldDividerPosition] as DividerWrapper).showDivider = show
+ }
+
+ /**
+ * Performs the update in the model.
+ *
+ * * update the favorite field of the [ControlInterface]
+ * * update the fields of the [DividerWrapper]
+ * * move the corresponding element in [elements]
+ *
+ * It may emit the following signals:
+ * * [RecyclerView.Adapter.notifyItemChanged] if a [ControlInterface.favorite] has changed
+ * (in the new position) or if the information in [DividerWrapper] has changed (in the
+ * old position).
+ * * [RecyclerView.Adapter.notifyItemMoved]
+ * * [FavoritesModelCallback.onNoneChanged] whenever we go from 1 to 0 favorites and back
+ * * [ControlsModel.ControlsModelCallback.onFirstChange] upon the first change in the model
+ */
+ private fun onMoveItemInternal(from: Int, to: Int) {
+ if (from == dividerPosition) return // divider does not move
+ var changed = false
+ if (from < dividerPosition && to >= dividerPosition ||
+ from > dividerPosition && to <= dividerPosition) {
+ if (from < dividerPosition && to >= dividerPosition) {
+ // favorite to not favorite
+ (elements[from] as ControlInfoWrapper).favorite = false
+ } else if (from > dividerPosition && to <= dividerPosition) {
+ // not favorite to favorite
+ (elements[from] as ControlInfoWrapper).favorite = true
+ }
+ changed = true
+ updateDivider(from, to)
+ }
+ moveElement(from, to)
+ adapter?.notifyItemMoved(from, to)
+ if (changed) {
+ adapter?.notifyItemChanged(to, Any())
+ }
+ if (!modified) {
+ modified = true
+ favoritesModelCallback.onFirstChange()
+ }
+ }
+
+ private fun updateDivider(from: Int, to: Int) {
+ var dividerChanged = false
+ val oldDividerPosition = dividerPosition
+ if (from < dividerPosition && to >= dividerPosition) { // favorite to not favorite
+ dividerPosition--
+ if (dividerPosition == 0) {
+ updateDividerNone(oldDividerPosition, true)
+ dividerChanged = true
+ }
+ if (dividerPosition == elements.size - 2) {
+ updateDividerShow(oldDividerPosition, true)
+ dividerChanged = true
+ }
+ } else if (from > dividerPosition && to <= dividerPosition) { // not favorite to favorite
+ dividerPosition++
+ if (dividerPosition == 1) {
+ updateDividerNone(oldDividerPosition, false)
+ dividerChanged = true
+ }
+ if (dividerPosition == elements.size - 1) {
+ updateDividerShow(oldDividerPosition, false)
+ dividerChanged = true
+ }
+ }
+ if (dividerChanged) {
+ adapter?.notifyItemChanged(oldDividerPosition)
+ }
+ }
+
+ private fun moveElement(from: Int, to: Int) {
+ if (from < to) {
+ for (i in from until to) {
+ Collections.swap(elements, i, i + 1)
+ }
+ } else {
+ for (i in from downTo to + 1) {
+ Collections.swap(elements, i, i - 1)
+ }
+ }
+ }
+
+ /**
+ * Touch helper to facilitate dragging in the [RecyclerView].
+ *
+ * Only views above the divider line (favorites) can be dragged or accept drops.
+ */
+ val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(0, 0) {
+
+ private val MOVEMENT = ItemTouchHelper.UP or
+ ItemTouchHelper.DOWN or
+ ItemTouchHelper.LEFT or
+ ItemTouchHelper.RIGHT
+
+ override fun onMove(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder
+ ): Boolean {
+ onMoveItem(viewHolder.adapterPosition, target.adapterPosition)
+ return true
+ }
+
+ override fun getMovementFlags(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder
+ ): Int {
+ if (viewHolder.adapterPosition < dividerPosition) {
+ return ItemTouchHelper.Callback.makeMovementFlags(MOVEMENT, 0)
+ } else {
+ return ItemTouchHelper.Callback.makeMovementFlags(0, 0)
+ }
+ }
+
+ override fun canDropOver(
+ recyclerView: RecyclerView,
+ current: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder
+ ): Boolean {
+ return target.adapterPosition < dividerPosition
+ }
+
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
+
+ override fun isItemViewSwipeEnabled() = false
+ }
+
+ interface FavoritesModelCallback : ControlsModel.ControlsModelCallback {
+ fun onNoneChanged(showNoFavorites: Boolean)
+ }
+} \ 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 39fbdcf7625f..fab6fc7357dd 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -33,16 +33,16 @@ import android.os.Process
import android.provider.Settings
import android.service.controls.Control
import android.service.controls.actions.ControlAction
-import android.util.TypedValue
import android.util.Log
-import android.view.animation.AccelerateInterpolator
-import android.view.animation.DecelerateInterpolator
+import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.View.MeasureSpec
import android.view.ViewGroup
import android.view.WindowManager
+import android.view.animation.AccelerateInterpolator
+import android.view.animation.DecelerateInterpolator
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
@@ -50,23 +50,21 @@ import android.widget.LinearLayout
import android.widget.ListPopupWindow
import android.widget.Space
import android.widget.TextView
+import com.android.systemui.R
import com.android.systemui.controls.ControlsServiceInfo
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.management.ControlsEditingActivity
import com.android.systemui.controls.management.ControlsFavoritingActivity
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.management.ControlsProviderSelectorActivity
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.R
-
import dagger.Lazy
-
import java.text.Collator
import java.util.function.Consumer
-
import javax.inject.Inject
import javax.inject.Singleton
@@ -212,14 +210,28 @@ class ControlsUiControllerImpl @Inject constructor (
}
private fun startFavoritingActivity(context: Context, si: StructureInfo) {
- val i = Intent(context, ControlsFavoritingActivity::class.java).apply {
+ startTargetedActivity(context, si, ControlsFavoritingActivity::class.java)
+ }
+
+ private fun startEditingActivity(context: Context, si: StructureInfo) {
+ startTargetedActivity(context, si, ControlsEditingActivity::class.java)
+ }
+
+ private fun startTargetedActivity(context: Context, si: StructureInfo, klazz: Class<*>) {
+ val i = Intent(context, klazz).apply {
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ putIntentExtras(i, si)
+ startActivity(context, i)
+ }
+
+ private fun putIntentExtras(intent: Intent, si: StructureInfo) {
+ intent.apply {
putExtra(ControlsFavoritingActivity.EXTRA_APP,
- controlsListingController.get().getAppLabel(si.componentName))
+ controlsListingController.get().getAppLabel(si.componentName))
putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure)
putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName)
- addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
}
- startActivity(context, i)
}
private fun startProviderSelectorActivity(context: Context) {
@@ -255,6 +267,7 @@ class ControlsUiControllerImpl @Inject constructor (
private fun createMenu() {
val items = arrayOf(
context.resources.getString(R.string.controls_menu_add),
+ context.resources.getString(R.string.controls_menu_edit),
"Reset"
)
var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items)
@@ -275,8 +288,10 @@ class ControlsUiControllerImpl @Inject constructor (
when (pos) {
// 0: Add Control
0 -> startFavoritingActivity(view.context, selectedStructure)
- // 1: TEMPORARY for reset controls
- 1 -> showResetConfirmation()
+ // 1: Edit controls
+ 1 -> startEditingActivity(view.context, selectedStructure)
+ // 2: TEMPORARY for reset controls
+ 2 -> showResetConfirmation()
else -> Log.w(ControlsUiController.TAG,
"Unsupported index ($pos) on 'more' menu selection")
}
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 8630570c4e70..f6ee46b0303a 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
@@ -87,9 +87,6 @@ class ControlsControllerImplTest : SysuiTestCase() {
private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo>
@Captor
- private lateinit var booleanConsumer: ArgumentCaptor<Consumer<Boolean>>
-
- @Captor
private lateinit var controlLoadCallbackCaptor:
ArgumentCaptor<ControlsBindingController.LoadCallback>
@Captor
@@ -936,4 +933,33 @@ class ControlsControllerImplTest : SysuiTestCase() {
verifyNoMoreInteractions(persistenceWrapper)
verifyNoMoreInteractions(auxiliaryPersistenceWrapper)
}
+
+ @Test
+ fun testGetFavoritesForStructure() {
+ controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
+ controller.replaceFavoritesForStructure(
+ TEST_STRUCTURE_INFO_2.copy(componentName = TEST_COMPONENT))
+ delayableExecutor.runAllReady()
+
+ assertEquals(TEST_STRUCTURE_INFO.controls,
+ controller.getFavoritesForStructure(TEST_COMPONENT, TEST_STRUCTURE))
+ assertEquals(TEST_STRUCTURE_INFO_2.controls,
+ controller.getFavoritesForStructure(TEST_COMPONENT, TEST_STRUCTURE_2))
+ }
+
+ @Test
+ fun testGetFavoritesForStructure_wrongStructure() {
+ controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
+ delayableExecutor.runAllReady()
+
+ assertTrue(controller.getFavoritesForStructure(TEST_COMPONENT, TEST_STRUCTURE_2).isEmpty())
+ }
+
+ @Test
+ fun testGetFavoritesForStructure_wrongComponent() {
+ controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
+ delayableExecutor.runAllReady()
+
+ assertTrue(controller.getFavoritesForStructure(TEST_COMPONENT_2, TEST_STRUCTURE).isEmpty())
+ }
}
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
index 5e0d28f6f795..236384b09514 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AllModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AllModelTest.kt
@@ -31,6 +31,8 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@@ -43,6 +45,8 @@ class AllModelTest : SysuiTestCase() {
@Mock
lateinit var pendingIntent: PendingIntent
+ @Mock
+ lateinit var controlsModelCallback: ControlsModel.ControlsModelCallback
val idPrefix = "controlId"
val favoritesIndices = listOf(7, 3, 1, 9)
@@ -84,7 +88,7 @@ class AllModelTest : SysuiTestCase() {
it in favoritesIndices
)
}
- model = AllModel(controls, favoritesList, EMPTY_STRING)
+ model = AllModel(controls, favoritesList, EMPTY_STRING, controlsModelCallback)
}
@Test
@@ -93,28 +97,28 @@ class AllModelTest : SysuiTestCase() {
// 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]),
+ ControlStatusWrapper(controls[0]),
+ ControlStatusWrapper(controls[3]),
+ ControlStatusWrapper(controls[6]),
+ ControlStatusWrapper(controls[9]),
ZoneNameWrapper("2"),
- ControlWrapper(controls[1]),
- ControlWrapper(controls[4]),
- ControlWrapper(controls[7]),
+ ControlStatusWrapper(controls[1]),
+ ControlStatusWrapper(controls[4]),
+ ControlStatusWrapper(controls[7]),
ZoneNameWrapper("0"),
- ControlWrapper(controls[2]),
- ControlWrapper(controls[5]),
- ControlWrapper(controls[8]),
+ ControlStatusWrapper(controls[2]),
+ ControlStatusWrapper(controls[5]),
+ ControlStatusWrapper(controls[8]),
ZoneNameWrapper(EMPTY_STRING),
- ControlWrapper(controls[10]),
- ControlWrapper(controls[11])
+ ControlStatusWrapper(controls[10]),
+ ControlStatusWrapper(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 {
+ private fun sameControl(controlInfo: ControlInfo, control: Control): Boolean {
return controlInfo.controlId == control.controlId &&
controlInfo.controlTitle == control.title &&
controlInfo.controlSubtitle == control.subtitle &&
@@ -124,10 +128,11 @@ class AllModelTest : SysuiTestCase() {
@Test
fun testAllEmpty_noHeader() {
val selected_controls = listOf(controls[10], controls[11])
- val new_model = AllModel(selected_controls, emptyList(), EMPTY_STRING)
+ val new_model = AllModel(selected_controls, emptyList(), EMPTY_STRING,
+ controlsModelCallback)
val expected = listOf(
- ControlWrapper(controls[10]),
- ControlWrapper(controls[11])
+ ControlStatusWrapper(controls[10]),
+ ControlStatusWrapper(controls[11])
)
expected.zip(new_model.elements).forEachIndexed { index, it ->
@@ -154,6 +159,8 @@ class AllModelTest : SysuiTestCase() {
model.favorites.zip(expectedFavorites).forEach {
assertTrue(sameControl(it.first, it.second))
}
+
+ verify(controlsModelCallback).onFirstChange()
}
@Test
@@ -163,10 +170,12 @@ class AllModelTest : SysuiTestCase() {
model.changeFavoriteStatus(id, true)
assertTrue(
(model.elements.first {
- it is ControlWrapper && it.controlStatus.control.controlId == id
- } as ControlWrapper)
+ it is ControlStatusWrapper && it.controlStatus.control.controlId == id
+ } as ControlStatusWrapper)
.controlStatus.favorite
)
+
+ verify(controlsModelCallback).onFirstChange()
}
@Test
@@ -180,6 +189,8 @@ class AllModelTest : SysuiTestCase() {
model.favorites.zip(expectedFavorites).forEach {
assertTrue(sameControl(it.first, it.second))
}
+
+ verify(controlsModelCallback, never()).onFirstChange()
}
@Test
@@ -194,6 +205,8 @@ class AllModelTest : SysuiTestCase() {
model.favorites.zip(expectedFavorites).forEach {
assertTrue(sameControl(it.first, it.second))
}
+
+ verify(controlsModelCallback).onFirstChange()
}
@Test
@@ -203,10 +216,12 @@ class AllModelTest : SysuiTestCase() {
model.changeFavoriteStatus(id, false)
assertFalse(
(model.elements.first {
- it is ControlWrapper && it.controlStatus.control.controlId == id
- } as ControlWrapper)
+ it is ControlStatusWrapper && it.controlStatus.control.controlId == id
+ } as ControlStatusWrapper)
.controlStatus.favorite
)
+
+ verify(controlsModelCallback).onFirstChange()
}
@Test
@@ -219,5 +234,7 @@ class AllModelTest : SysuiTestCase() {
model.favorites.zip(expectedFavorites).forEach {
assertTrue(sameControl(it.first, it.second))
}
+
+ verify(controlsModelCallback, never()).onFirstChange()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoriteModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoriteModelTest.kt
deleted file mode 100644
index c330b38fed42..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoriteModelTest.kt
+++ /dev/null
@@ -1,200 +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.controls.management
-
-import android.app.PendingIntent
-import android.content.ComponentName
-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 org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.MockitoAnnotations
-
-open class FavoriteModelTest : SysuiTestCase() {
-
- @Mock
- lateinit var pendingIntent: PendingIntent
- @Mock
- lateinit var allAdapter: ControlAdapter
- @Mock
- lateinit var favoritesAdapter: ControlAdapter
-
- val idPrefix = "controlId"
- val favoritesIndices = listOf(7, 3, 1, 9)
- val favoritesList = favoritesIndices.map { "controlId$it" }
- lateinit var controls: List<ControlStatus>
-
- lateinit var model: FavoriteModel
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
-
- // controlId0 --> zone = 0
- // controlId1 --> zone = 1, favorite
- // controlId2 --> zone = 2
- // controlId3 --> zone = 0, favorite
- // controlId4 --> zone = 1
- // controlId5 --> zone = 2
- // controlId6 --> zone = 0
- // controlId7 --> zone = 1, favorite
- // controlId8 --> zone = 2
- // controlId9 --> zone = 0, favorite
- controls = (0..9).map {
- ControlStatus(
- Control.StatelessBuilder("$idPrefix$it", pendingIntent)
- .setZone((it % 3).toString())
- .build(),
- ComponentName("", ""),
- it in favoritesIndices
- )
- }
-
- model = FavoriteModel(controls, favoritesList, favoritesAdapter, allAdapter)
- }
-}
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class FavoriteModelNonParametrizedTests : FavoriteModelTest() {
- @Test
- fun testAll() {
- // Zones are sorted alphabetically
- val expected = listOf(
- ZoneNameWrapper("0"),
- ControlWrapper(controls[0]),
- ControlWrapper(controls[3]),
- ControlWrapper(controls[6]),
- ControlWrapper(controls[9]),
- ZoneNameWrapper("1"),
- ControlWrapper(controls[1]),
- ControlWrapper(controls[4]),
- ControlWrapper(controls[7]),
- ZoneNameWrapper("2"),
- ControlWrapper(controls[2]),
- ControlWrapper(controls[5]),
- ControlWrapper(controls[8])
- )
- assertEquals(expected, model.all)
- }
-
- @Test
- fun testFavoritesInOrder() {
- val expected = favoritesIndices.map { ControlWrapper(controls[it]) }
- assertEquals(expected, model.favorites)
- }
-
- @Test
- fun testChangeFavoriteStatus_addFavorite() {
- val controlToAdd = 6
- model.changeFavoriteStatus("$idPrefix$controlToAdd", true)
-
- val pair = model.all.findControl(controlToAdd)
- pair?.let {
- assertTrue(it.second.favorite)
- assertEquals(it.second, model.favorites.last().controlStatus)
- verify(favoritesAdapter).notifyItemInserted(model.favorites.size - 1)
- verify(allAdapter).notifyItemChanged(it.first)
- verifyNoMoreInteractions(favoritesAdapter, allAdapter)
- } ?: run {
- fail("control not found")
- }
- }
-
- @Test
- fun testChangeFavoriteStatus_removeFavorite() {
- val controlToRemove = 3
- model.changeFavoriteStatus("$idPrefix$controlToRemove", false)
-
- val pair = model.all.findControl(controlToRemove)
- pair?.let {
- assertFalse(it.second.favorite)
- assertTrue(model.favorites.none {
- it.controlStatus.control.controlId == "$idPrefix$controlToRemove"
- })
- verify(favoritesAdapter).notifyItemRemoved(favoritesIndices.indexOf(controlToRemove))
- verify(allAdapter).notifyItemChanged(it.first)
- verifyNoMoreInteractions(favoritesAdapter, allAdapter)
- } ?: run {
- fail("control not found")
- }
- }
-
- @Test
- fun testChangeFavoriteStatus_sameStatus() {
- model.changeFavoriteStatus("${idPrefix}7", true)
- model.changeFavoriteStatus("${idPrefix}6", false)
-
- val expected = favoritesIndices.map { ControlWrapper(controls[it]) }
- assertEquals(expected, model.favorites)
-
- verifyNoMoreInteractions(favoritesAdapter, allAdapter)
- }
-
- private fun List<ElementWrapper>.findControl(controlIndex: Int): Pair<Int, ControlStatus>? {
- val index = indexOfFirst {
- it is ControlWrapper &&
- it.controlStatus.control.controlId == "$idPrefix$controlIndex"
- }
- return if (index == -1) null else index to (get(index) as ControlWrapper).controlStatus
- }
-}
-
-@SmallTest
-@RunWith(Parameterized::class)
-class FavoriteModelParameterizedTest(val from: Int, val to: Int) : FavoriteModelTest() {
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0} -> {1}")
- fun data(): Collection<Array<Int>> {
- return (0..3).flatMap { from ->
- (0..3).map { to ->
- arrayOf(from, to)
- }
- }.filterNot { it[0] == it[1] }
- }
- }
-
- @Test
- fun testMoveItem() {
- val originalFavorites = model.favorites.toList()
- val originalFavoritesIds =
- model.favorites.map { it.controlStatus.control.controlId }.toSet()
- model.onMoveItem(from, to)
- assertEquals(originalFavorites[from], model.favorites[to])
- // Check that we still have the same favorites
- assertEquals(originalFavoritesIds,
- model.favorites.map { it.controlStatus.control.controlId }.toSet())
-
- verify(favoritesAdapter).notifyItemMoved(from, to)
-
- verifyNoMoreInteractions(allAdapter, favoritesAdapter)
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoritesModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoritesModelTest.kt
new file mode 100644
index 000000000000..ce33a8d49fac
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoritesModelTest.kt
@@ -0,0 +1,291 @@
+/*
+ * 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.ComponentName
+import android.testing.AndroidTestingRunner
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlInterface
+import com.android.systemui.controls.controller.ControlInfo
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class FavoritesModelTest : SysuiTestCase() {
+
+ companion object {
+ private val TEST_COMPONENT = ComponentName.unflattenFromString("test_pkg/.test_cls")!!
+ private val ID_PREFIX = "control"
+ private val INITIAL_FAVORITES = (0..5).map {
+ ControlInfo("$ID_PREFIX$it", "title$it", "subtitle$it", it)
+ }
+ }
+
+ @Mock
+ private lateinit var callback: FavoritesModel.FavoritesModelCallback
+ @Mock
+ private lateinit var adapter: RecyclerView.Adapter<*>
+ private lateinit var model: FavoritesModel
+ private lateinit var dividerWrapper: DividerWrapper
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ model = FavoritesModel(TEST_COMPONENT, INITIAL_FAVORITES, callback)
+ model.attachAdapter(adapter)
+ dividerWrapper = model.elements.first { it is DividerWrapper } as DividerWrapper
+ }
+
+ @After
+ fun testListConsistency() {
+ assertEquals(INITIAL_FAVORITES.size + 1, model.elements.toSet().size)
+ val dividerIndex = getDividerPosition()
+ model.elements.forEachIndexed { index, element ->
+ if (index == dividerIndex) {
+ assertEquals(dividerWrapper, element)
+ } else {
+ element as ControlInterface
+ assertEquals(index < dividerIndex, element.favorite)
+ }
+ }
+ assertEquals(model.favorites, model.elements.take(dividerIndex).map {
+ (it as ControlInfoWrapper).controlInfo
+ })
+ }
+
+ @Test
+ fun testInitialElements() {
+ val expected = INITIAL_FAVORITES.map {
+ ControlInfoWrapper(TEST_COMPONENT, it, true)
+ } + DividerWrapper()
+ assertEquals(expected, model.elements)
+ }
+
+ @Test
+ fun testFavorites() {
+ assertEquals(INITIAL_FAVORITES, model.favorites)
+ }
+
+ @Test
+ fun testRemoveFavorite_notInFavorites() {
+ val removed = 4
+ val id = "$ID_PREFIX$removed"
+
+ model.changeFavoriteStatus(id, false)
+
+ assertTrue(model.favorites.none { it.controlId == id })
+
+ verify(callback).onFirstChange()
+ }
+
+ @Test
+ fun testRemoveFavorite_endOfElements() {
+ val removed = 4
+ val id = "$ID_PREFIX$removed"
+ model.changeFavoriteStatus(id, false)
+
+ assertEquals(ControlInfoWrapper(
+ TEST_COMPONENT, INITIAL_FAVORITES[4], false), model.elements.last())
+ verify(callback).onFirstChange()
+ }
+
+ @Test
+ fun testRemoveFavorite_adapterNotified() {
+ val removed = 4
+ val id = "$ID_PREFIX$removed"
+ model.changeFavoriteStatus(id, false)
+
+ val lastPos = model.elements.size - 1
+ verify(adapter).notifyItemChanged(eq(lastPos), any(Any::class.java))
+ verify(adapter).notifyItemMoved(removed, lastPos)
+
+ verify(callback).onFirstChange()
+ }
+
+ @Test
+ fun testRemoveFavorite_dividerMovedBack() {
+ val oldDividerPosition = getDividerPosition()
+ val removed = 4
+ val id = "$ID_PREFIX$removed"
+ model.changeFavoriteStatus(id, false)
+
+ assertEquals(oldDividerPosition - 1, getDividerPosition())
+
+ verify(callback).onFirstChange()
+ }
+
+ @Test
+ fun testRemoveFavorite_ShowDivider() {
+ val oldDividerPosition = getDividerPosition()
+ val removed = 4
+ val id = "$ID_PREFIX$removed"
+ model.changeFavoriteStatus(id, false)
+
+ assertTrue(dividerWrapper.showDivider)
+ verify(adapter).notifyItemChanged(oldDividerPosition)
+
+ verify(callback).onFirstChange()
+ }
+
+ @Test
+ fun testDoubleRemove_onlyOnce() {
+ val removed = 4
+ val id = "$ID_PREFIX$removed"
+ model.changeFavoriteStatus(id, false)
+ model.changeFavoriteStatus(id, false)
+
+ verify(adapter /* only once */).notifyItemChanged(anyInt(), any(Any::class.java))
+ verify(adapter /* only once */).notifyItemMoved(anyInt(), anyInt())
+ verify(adapter /* only once (divider) */).notifyItemChanged(anyInt())
+
+ verify(callback).onFirstChange()
+ }
+
+ @Test
+ fun testRemoveTwo_InSameOrder() {
+ val removedFirst = 3
+ val removedSecond = 0
+ model.changeFavoriteStatus("$ID_PREFIX$removedFirst", false)
+ model.changeFavoriteStatus("$ID_PREFIX$removedSecond", false)
+
+ assertEquals(listOf(
+ ControlInfoWrapper(TEST_COMPONENT, INITIAL_FAVORITES[removedFirst], false),
+ ControlInfoWrapper(TEST_COMPONENT, INITIAL_FAVORITES[removedSecond], false)
+ ), model.elements.takeLast(2))
+
+ verify(callback).onFirstChange()
+ }
+
+ @Test
+ fun testRemoveAll_showNone() {
+ INITIAL_FAVORITES.forEach {
+ model.changeFavoriteStatus(it.controlId, false)
+ }
+ assertEquals(dividerWrapper, model.elements.first())
+ assertTrue(dividerWrapper.showNone)
+ verify(adapter, times(2)).notifyItemChanged(anyInt()) // divider
+ verify(callback).onNoneChanged(true)
+
+ verify(callback).onFirstChange()
+ }
+
+ @Test
+ fun testAddFavorite_movedToEnd() {
+ val added = 2
+ val id = "$ID_PREFIX$added"
+ model.changeFavoriteStatus(id, false)
+ model.changeFavoriteStatus(id, true)
+
+ assertEquals(id, model.favorites.last().controlId)
+
+ verify(callback).onFirstChange()
+ }
+
+ @Test
+ fun testAddFavorite_onlyOnce() {
+ val added = 2
+ val id = "$ID_PREFIX$added"
+ model.changeFavoriteStatus(id, false)
+ model.changeFavoriteStatus(id, true)
+ model.changeFavoriteStatus(id, true)
+
+ // Once for remove and once for add
+ verify(adapter, times(2)).notifyItemChanged(anyInt(), any(Any::class.java))
+ verify(adapter, times(2)).notifyItemMoved(anyInt(), anyInt())
+
+ verify(callback).onFirstChange()
+ }
+
+ @Test
+ fun testAddFavorite_notRemoved() {
+ val added = 2
+ val id = "$ID_PREFIX$added"
+ model.changeFavoriteStatus(id, true)
+
+ verifyNoMoreInteractions(adapter)
+
+ verify(callback, never()).onFirstChange()
+ }
+
+ @Test
+ fun testAddOnlyRemovedFavorite_dividerStopsShowing() {
+ val added = 2
+ val id = "$ID_PREFIX$added"
+ model.changeFavoriteStatus(id, false)
+ model.changeFavoriteStatus(id, true)
+
+ assertFalse(dividerWrapper.showDivider)
+ val inOrder = inOrder(adapter)
+ inOrder.verify(adapter).notifyItemChanged(model.elements.size - 1)
+ inOrder.verify(adapter).notifyItemChanged(model.elements.size - 2)
+
+ verify(callback).onFirstChange()
+ }
+
+ @Test
+ fun testAddFirstFavorite_dividerNotShowsNone() {
+ INITIAL_FAVORITES.forEach {
+ model.changeFavoriteStatus(it.controlId, false)
+ }
+
+ verify(callback).onNoneChanged(true)
+
+ model.changeFavoriteStatus("${ID_PREFIX}3", true)
+ assertEquals(1, getDividerPosition())
+
+ verify(callback).onNoneChanged(false)
+
+ verify(callback).onFirstChange()
+ }
+
+ @Test
+ fun testMoveBetweenFavorites() {
+ val from = 2
+ val to = 4
+
+ model.onMoveItem(from, to)
+ assertEquals(
+ listOf(0, 1, 3, 4, 2, 5).map { "$ID_PREFIX$it" },
+ model.favorites.map(ControlInfo::controlId)
+ )
+ verify(adapter).notifyItemMoved(from, to)
+ verify(adapter, never()).notifyItemChanged(anyInt(), any(Any::class.java))
+
+ verify(callback).onFirstChange()
+ }
+
+ private fun getDividerPosition(): Int = model.elements.indexOf(dividerWrapper)
+} \ No newline at end of file