Merge "Add controls rearrange activity" into rvc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
0935694b5a
@@ -679,6 +679,15 @@
|
|||||||
android:visibleToInstantApps="true">
|
android:visibleToInstantApps="true">
|
||||||
</activity>
|
</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"
|
<activity android:name=".controls.management.ControlsFavoritingActivity"
|
||||||
android:theme="@style/Theme.ControlsManagement"
|
android:theme="@style/Theme.ControlsManagement"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -14,18 +14,11 @@
|
|||||||
~ See the License for the specific language governing permissions and
|
~ See the License for the specific language governing permissions and
|
||||||
~ limitations under the License.
|
~ limitations under the License.
|
||||||
-->
|
-->
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1"
|
android:layout_marginTop="@dimen/controls_management_list_margin"
|
||||||
android:orientation="vertical"
|
/>
|
||||||
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>
|
|
||||||
|
|||||||
27
packages/SystemUI/res/layout/controls_management_editing.xml
Normal file
27
packages/SystemUI/res/layout/controls_management_editing.xml
Normal file
@@ -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"
|
||||||
|
/>
|
||||||
|
|
||||||
@@ -2670,8 +2670,11 @@
|
|||||||
<string name="controls_favorite_default_title">Controls</string>
|
<string name="controls_favorite_default_title">Controls</string>
|
||||||
<!-- Controls management controls screen subtitle [CHAR LIMIT=NONE] -->
|
<!-- Controls management controls screen subtitle [CHAR LIMIT=NONE] -->
|
||||||
<string name="controls_favorite_subtitle">Choose controls to access from the power menu</string>
|
<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] -->
|
<!-- Controls management editing screen, user direction for rearranging controls [CHAR LIMIT=NONE] -->
|
||||||
<string name="controls_favorite_rearrange">Hold and drag a control to move it</string>
|
<string name="controls_favorite_rearrange">Hold & 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] -->
|
<!-- 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>
|
<string name="controls_favorite_load_error">The list of all controls could not be loaded.</string>
|
||||||
|
|||||||
@@ -18,10 +18,34 @@ package com.android.systemui.controls
|
|||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.service.controls.Control
|
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(
|
data class ControlStatus(
|
||||||
val control: Control,
|
val control: Control,
|
||||||
val component: ComponentName,
|
override val component: ComponentName,
|
||||||
var favorite: Boolean,
|
override var favorite: Boolean,
|
||||||
val removed: Boolean = false
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package com.android.systemui.controls.controller
|
package com.android.systemui.controls.controller
|
||||||
|
|
||||||
|
import android.service.controls.Control
|
||||||
import android.service.controls.DeviceTypes
|
import android.service.controls.DeviceTypes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +40,14 @@ data class ControlInfo(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SEPARATOR = ":"
|
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 {
|
override fun toString(): String {
|
||||||
return "$SEPARATOR$controlId$SEPARATOR$controlTitle$SEPARATOR$deviceType"
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,18 @@ interface ControlsController : UserAwareController {
|
|||||||
*/
|
*/
|
||||||
fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo>
|
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
|
* Adds a single favorite to a given component and structure
|
||||||
* @param componentName the name of the service that provides the [Control]
|
* @param componentName the name of the service that provides the [Control]
|
||||||
|
|||||||
@@ -544,6 +544,15 @@ class ControlsControllerImpl @Inject constructor (
|
|||||||
override fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo> =
|
override fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo> =
|
||||||
Favorites.getStructuresForComponent(componentName)
|
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>) {
|
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
|
||||||
pw.println("ControlsController state:")
|
pw.println("ControlsController state:")
|
||||||
pw.println(" Available: $available")
|
pw.println(" Available: $available")
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import com.android.systemui.controls.controller.ControlsBindingControllerImpl
|
|||||||
import com.android.systemui.controls.controller.ControlsController
|
import com.android.systemui.controls.controller.ControlsController
|
||||||
import com.android.systemui.controls.controller.ControlsControllerImpl
|
import com.android.systemui.controls.controller.ControlsControllerImpl
|
||||||
import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper
|
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.ControlsFavoritingActivity
|
||||||
import com.android.systemui.controls.management.ControlsListingController
|
import com.android.systemui.controls.management.ControlsListingController
|
||||||
import com.android.systemui.controls.management.ControlsListingControllerImpl
|
import com.android.systemui.controls.management.ControlsListingControllerImpl
|
||||||
@@ -71,6 +72,13 @@ abstract class ControlsModule {
|
|||||||
activity: ControlsFavoritingActivity
|
activity: ControlsFavoritingActivity
|
||||||
): Activity
|
): Activity
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ClassKey(ControlsEditingActivity::class)
|
||||||
|
abstract fun provideControlsEditingActivity(
|
||||||
|
activity: ControlsEditingActivity
|
||||||
|
): Activity
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ClassKey(ControlsRequestDialog::class)
|
@ClassKey(ControlsRequestDialog::class)
|
||||||
|
|||||||
@@ -37,23 +37,22 @@ import com.android.systemui.controls.controller.ControlInfo
|
|||||||
* @property controls List of controls as returned by loading
|
* @property controls List of controls as returned by loading
|
||||||
* @property initialFavoriteIds sorted ids of favorite controls.
|
* @property initialFavoriteIds sorted ids of favorite controls.
|
||||||
* @property noZoneString text to use as header for all controls that have blank or `null` zone.
|
* @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(
|
class AllModel(
|
||||||
private val controls: List<ControlStatus>,
|
private val controls: List<ControlStatus>,
|
||||||
initialFavoriteIds: List<String>,
|
initialFavoriteIds: List<String>,
|
||||||
private val emptyZoneString: CharSequence
|
private val emptyZoneString: CharSequence,
|
||||||
|
private val controlsModelCallback: ControlsModel.ControlsModelCallback
|
||||||
) : ControlsModel {
|
) : ControlsModel {
|
||||||
|
|
||||||
override val favorites: List<ControlInfo.Builder>
|
private var modified = false
|
||||||
|
|
||||||
|
override val favorites: List<ControlInfo>
|
||||||
get() = favoriteIds.mapNotNull { id ->
|
get() = favoriteIds.mapNotNull { id ->
|
||||||
val control = controls.firstOrNull { it.control.controlId == id }?.control
|
val control = controls.firstOrNull { it.control.controlId == id }?.control
|
||||||
control?.let {
|
control?.let {
|
||||||
ControlInfo.Builder().apply {
|
ControlInfo.fromControl(it)
|
||||||
controlId = it.controlId
|
|
||||||
controlTitle = it.title
|
|
||||||
controlSubtitle = it.subtitle
|
|
||||||
deviceType = it.deviceType
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,14 +65,18 @@ class AllModel(
|
|||||||
|
|
||||||
override fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
|
override fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
|
||||||
val toChange = elements.firstOrNull {
|
val toChange = elements.firstOrNull {
|
||||||
it is ControlWrapper && it.controlStatus.control.controlId == controlId
|
it is ControlStatusWrapper && it.controlStatus.control.controlId == controlId
|
||||||
} as ControlWrapper?
|
} as ControlStatusWrapper?
|
||||||
if (favorite == toChange?.controlStatus?.favorite) return
|
if (favorite == toChange?.controlStatus?.favorite) return
|
||||||
if (favorite) {
|
val changed: Boolean = if (favorite) {
|
||||||
favoriteIds.add(controlId)
|
favoriteIds.add(controlId)
|
||||||
} else {
|
} else {
|
||||||
favoriteIds.remove(controlId)
|
favoriteIds.remove(controlId)
|
||||||
}
|
}
|
||||||
|
if (changed && !modified) {
|
||||||
|
modified = true
|
||||||
|
controlsModelCallback.onFirstChange()
|
||||||
|
}
|
||||||
toChange?.let {
|
toChange?.let {
|
||||||
it.controlStatus.favorite = favorite
|
it.controlStatus.favorite = favorite
|
||||||
}
|
}
|
||||||
@@ -84,9 +87,9 @@ class AllModel(
|
|||||||
it.control.zone ?: ""
|
it.control.zone ?: ""
|
||||||
}
|
}
|
||||||
val output = mutableListOf<ElementWrapper>()
|
val output = mutableListOf<ElementWrapper>()
|
||||||
var emptyZoneValues: Sequence<ControlWrapper>? = null
|
var emptyZoneValues: Sequence<ControlStatusWrapper>? = null
|
||||||
for (zoneName in map.orderedKeys) {
|
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)) {
|
if (TextUtils.isEmpty(zoneName)) {
|
||||||
emptyZoneValues = values
|
emptyZoneValues = values
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import android.widget.TextView
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.controls.ControlInterface
|
||||||
import com.android.systemui.controls.ui.RenderInfo
|
import com.android.systemui.controls.ui.RenderInfo
|
||||||
|
|
||||||
private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
|
private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
|
||||||
@@ -35,11 +36,10 @@ private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
|
|||||||
/**
|
/**
|
||||||
* Adapter for binding [Control] information to views.
|
* 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.
|
* [changeFavoritesModel]. This allows for updating the model if there's a reload.
|
||||||
*
|
*
|
||||||
* @param layoutInflater an inflater for the views in the containing [RecyclerView]
|
* @property elevation elevation of each control view
|
||||||
* @param onlyFavorites set to true to only display favorites instead of all controls
|
|
||||||
*/
|
*/
|
||||||
class ControlAdapter(
|
class ControlAdapter(
|
||||||
private val elevation: Float
|
private val elevation: Float
|
||||||
@@ -48,11 +48,12 @@ class ControlAdapter(
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TYPE_ZONE = 0
|
private const val TYPE_ZONE = 0
|
||||||
private const val TYPE_CONTROL = 1
|
private const val TYPE_CONTROL = 1
|
||||||
|
private const val TYPE_DIVIDER = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
val spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
val spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||||
override fun getSpanSize(position: Int): Int {
|
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 -> {
|
TYPE_ZONE -> {
|
||||||
ZoneHolder(layoutInflater.inflate(R.layout.controls_zone_header, parent, false))
|
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")
|
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 {
|
override fun getItemViewType(position: Int): Int {
|
||||||
model?.let {
|
model?.let {
|
||||||
return when (it.elements.get(position)) {
|
return when (it.elements.get(position)) {
|
||||||
is ZoneNameWrapper -> TYPE_ZONE
|
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")
|
} ?: 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
|
* Bind the data from the model into the view
|
||||||
*/
|
*/
|
||||||
abstract fun bindData(wrapper: ElementWrapper)
|
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
|
* @param favoriteCallback this callback will be called whenever the favorite state of the
|
||||||
* [Control] this view represents changes.
|
* [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 icon: ImageView = itemView.requireViewById(R.id.icon)
|
||||||
private val title: TextView = itemView.requireViewById(R.id.title)
|
private val title: TextView = itemView.requireViewById(R.id.title)
|
||||||
private val subtitle: TextView = itemView.requireViewById(R.id.subtitle)
|
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) {
|
override fun bindData(wrapper: ElementWrapper) {
|
||||||
wrapper as ControlWrapper
|
wrapper as ControlInterface
|
||||||
val data = wrapper.controlStatus
|
val renderInfo = getRenderInfo(wrapper.component, wrapper.deviceType)
|
||||||
val renderInfo = getRenderInfo(data.component, data.control.deviceType)
|
title.text = wrapper.title
|
||||||
title.text = data.control.title
|
subtitle.text = wrapper.subtitle
|
||||||
subtitle.text = data.control.subtitle
|
favorite.isChecked = wrapper.favorite
|
||||||
favorite.isChecked = data.favorite
|
removed.text = if (wrapper.removed) "Removed" else ""
|
||||||
removed.text = if (data.removed) "Removed" else ""
|
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
favorite.isChecked = !favorite.isChecked
|
favorite.isChecked = !favorite.isChecked
|
||||||
favoriteCallback(data.control.controlId, favorite.isChecked)
|
favoriteCallback(wrapper.controlId, favorite.isChecked)
|
||||||
}
|
}
|
||||||
applyRenderInfo(renderInfo)
|
applyRenderInfo(renderInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateFavorite(favorite: Boolean) {
|
||||||
|
this.favorite.isChecked = favorite
|
||||||
|
}
|
||||||
|
|
||||||
private fun getRenderInfo(
|
private fun getRenderInfo(
|
||||||
component: ComponentName,
|
component: ComponentName,
|
||||||
@DeviceTypes.DeviceType deviceType: Int
|
@DeviceTypes.DeviceType deviceType: Int
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,7 @@ class ControlsFavoritingActivity @Inject constructor(
|
|||||||
|
|
||||||
// If provided, show this structure page first
|
// If provided, show this structure page first
|
||||||
const val EXTRA_STRUCTURE = "extra_structure"
|
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_PREFS_KEY = Prefs.Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT
|
||||||
private const val TOOLTIP_MAX_SHOWN = 2
|
private const val TOOLTIP_MAX_SHOWN = 2
|
||||||
}
|
}
|
||||||
@@ -131,6 +132,12 @@ class ControlsFavoritingActivity @Inject constructor(
|
|||||||
currentUserTracker.startTracking()
|
currentUserTracker.startTracking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val controlsModelCallback = object : ControlsModel.ControlsModelCallback {
|
||||||
|
override fun onFirstChange() {
|
||||||
|
doneButton.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadControls() {
|
private fun loadControls() {
|
||||||
component?.let {
|
component?.let {
|
||||||
statusText.text = resources.getText(com.android.internal.R.string.loading)
|
statusText.text = resources.getText(com.android.internal.R.string.loading)
|
||||||
@@ -142,15 +149,20 @@ class ControlsFavoritingActivity @Inject constructor(
|
|||||||
val error = data.errorOnLoad
|
val error = data.errorOnLoad
|
||||||
val controlsByStructure = allControls.groupBy { it.control.structure ?: "" }
|
val controlsByStructure = allControls.groupBy { it.control.structure ?: "" }
|
||||||
listOfStructures = controlsByStructure.map {
|
listOfStructures = controlsByStructure.map {
|
||||||
StructureContainer(it.key, AllModel(it.value, favoriteKeys, emptyZoneString))
|
StructureContainer(it.key, AllModel(
|
||||||
|
it.value, favoriteKeys, emptyZoneString, controlsModelCallback))
|
||||||
}.sortedWith(comparator)
|
}.sortedWith(comparator)
|
||||||
|
|
||||||
val structureIndex = listOfStructures.indexOfFirst {
|
val structureIndex = listOfStructures.indexOfFirst {
|
||||||
sc -> sc.structureName == structureExtra
|
sc -> sc.structureName == structureExtra
|
||||||
}.let { if (it == -1) 0 else it }
|
}.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 {
|
executor.execute {
|
||||||
doneButton.isEnabled = true
|
|
||||||
structurePager.adapter = StructureAdapter(listOfStructures)
|
structurePager.adapter = StructureAdapter(listOfStructures)
|
||||||
structurePager.setCurrentItem(structureIndex)
|
structurePager.setCurrentItem(structureIndex)
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -275,7 +287,7 @@ class ControlsFavoritingActivity @Inject constructor(
|
|||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
if (component == null) return@setOnClickListener
|
if (component == null) return@setOnClickListener
|
||||||
listOfStructures.forEach {
|
listOfStructures.forEach {
|
||||||
val favoritesForStorage = it.model.favorites.map { it.build() }
|
val favoritesForStorage = it.model.favorites
|
||||||
controller.replaceFavoritesForStructure(
|
controller.replaceFavoritesForStructure(
|
||||||
StructureInfo(component!!, it.structureName, favoritesForStorage)
|
StructureInfo(component!!, it.structureName, favoritesForStorage)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
|
|
||||||
package com.android.systemui.controls.management
|
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.ControlStatus
|
||||||
import com.android.systemui.controls.controller.ControlInfo
|
import com.android.systemui.controls.controller.ControlInfo
|
||||||
|
|
||||||
@@ -27,12 +30,12 @@ import com.android.systemui.controls.controller.ControlInfo
|
|||||||
interface ControlsModel {
|
interface ControlsModel {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of favorites (builders) in order.
|
* List of favorites in order.
|
||||||
*
|
*
|
||||||
* This should be obtained prior to storing the favorites using
|
* This should be obtained prior to storing the favorites using
|
||||||
* [ControlsController.replaceFavoritesForComponent].
|
* [ControlsController.replaceFavoritesForComponent].
|
||||||
*/
|
*/
|
||||||
val favorites: List<ControlInfo.Builder>
|
val favorites: List<ControlInfo>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of all the elements to display by the corresponding [RecyclerView].
|
* 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.
|
* Move an item (in elements) from one position to another.
|
||||||
*/
|
*/
|
||||||
fun onMoveItem(from: Int, to: Int) {}
|
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].
|
* [ControlAdapter].
|
||||||
*/
|
*/
|
||||||
sealed class ElementWrapper
|
sealed class ElementWrapper
|
||||||
|
|
||||||
data class ZoneNameWrapper(val zoneName: CharSequence) : ElementWrapper()
|
data class ZoneNameWrapper(val zoneName: CharSequence) : ElementWrapper()
|
||||||
data class ControlWrapper(val controlStatus: ControlStatus) : ElementWrapper()
|
|
||||||
|
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()
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,16 +33,16 @@ import android.os.Process
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.service.controls.Control
|
import android.service.controls.Control
|
||||||
import android.service.controls.actions.ControlAction
|
import android.service.controls.actions.ControlAction
|
||||||
import android.util.TypedValue
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.animation.AccelerateInterpolator
|
import android.util.TypedValue
|
||||||
import android.view.animation.DecelerateInterpolator
|
|
||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.View.MeasureSpec
|
import android.view.View.MeasureSpec
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.view.animation.AccelerateInterpolator
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@@ -50,23 +50,21 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.ListPopupWindow
|
import android.widget.ListPopupWindow
|
||||||
import android.widget.Space
|
import android.widget.Space
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import com.android.systemui.R
|
||||||
import com.android.systemui.controls.ControlsServiceInfo
|
import com.android.systemui.controls.ControlsServiceInfo
|
||||||
import com.android.systemui.controls.controller.ControlInfo
|
import com.android.systemui.controls.controller.ControlInfo
|
||||||
import com.android.systemui.controls.controller.ControlsController
|
import com.android.systemui.controls.controller.ControlsController
|
||||||
import com.android.systemui.controls.controller.StructureInfo
|
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.ControlsFavoritingActivity
|
||||||
import com.android.systemui.controls.management.ControlsListingController
|
import com.android.systemui.controls.management.ControlsListingController
|
||||||
import com.android.systemui.controls.management.ControlsProviderSelectorActivity
|
import com.android.systemui.controls.management.ControlsProviderSelectorActivity
|
||||||
import com.android.systemui.dagger.qualifiers.Background
|
import com.android.systemui.dagger.qualifiers.Background
|
||||||
import com.android.systemui.dagger.qualifiers.Main
|
import com.android.systemui.dagger.qualifiers.Main
|
||||||
import com.android.systemui.util.concurrency.DelayableExecutor
|
import com.android.systemui.util.concurrency.DelayableExecutor
|
||||||
import com.android.systemui.R
|
|
||||||
|
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
|
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -212,16 +210,30 @@ class ControlsUiControllerImpl @Inject constructor (
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startFavoritingActivity(context: Context, si: StructureInfo) {
|
private fun startFavoritingActivity(context: Context, si: StructureInfo) {
|
||||||
val i = Intent(context, ControlsFavoritingActivity::class.java).apply {
|
startTargetedActivity(context, si, ControlsFavoritingActivity::class.java)
|
||||||
putExtra(ControlsFavoritingActivity.EXTRA_APP,
|
}
|
||||||
controlsListingController.get().getAppLabel(si.componentName))
|
|
||||||
putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure)
|
private fun startEditingActivity(context: Context, si: StructureInfo) {
|
||||||
putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName)
|
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)
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
|
putIntentExtras(i, si)
|
||||||
startActivity(context, i)
|
startActivity(context, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun putIntentExtras(intent: Intent, si: StructureInfo) {
|
||||||
|
intent.apply {
|
||||||
|
putExtra(ControlsFavoritingActivity.EXTRA_APP,
|
||||||
|
controlsListingController.get().getAppLabel(si.componentName))
|
||||||
|
putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure)
|
||||||
|
putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startProviderSelectorActivity(context: Context) {
|
private fun startProviderSelectorActivity(context: Context) {
|
||||||
val i = Intent(context, ControlsProviderSelectorActivity::class.java).apply {
|
val i = Intent(context, ControlsProviderSelectorActivity::class.java).apply {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
@@ -255,6 +267,7 @@ class ControlsUiControllerImpl @Inject constructor (
|
|||||||
private fun createMenu() {
|
private fun createMenu() {
|
||||||
val items = arrayOf(
|
val items = arrayOf(
|
||||||
context.resources.getString(R.string.controls_menu_add),
|
context.resources.getString(R.string.controls_menu_add),
|
||||||
|
context.resources.getString(R.string.controls_menu_edit),
|
||||||
"Reset"
|
"Reset"
|
||||||
)
|
)
|
||||||
var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items)
|
var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items)
|
||||||
@@ -275,8 +288,10 @@ class ControlsUiControllerImpl @Inject constructor (
|
|||||||
when (pos) {
|
when (pos) {
|
||||||
// 0: Add Control
|
// 0: Add Control
|
||||||
0 -> startFavoritingActivity(view.context, selectedStructure)
|
0 -> startFavoritingActivity(view.context, selectedStructure)
|
||||||
// 1: TEMPORARY for reset controls
|
// 1: Edit controls
|
||||||
1 -> showResetConfirmation()
|
1 -> startEditingActivity(view.context, selectedStructure)
|
||||||
|
// 2: TEMPORARY for reset controls
|
||||||
|
2 -> showResetConfirmation()
|
||||||
else -> Log.w(ControlsUiController.TAG,
|
else -> Log.w(ControlsUiController.TAG,
|
||||||
"Unsupported index ($pos) on 'more' menu selection")
|
"Unsupported index ($pos) on 'more' menu selection")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,9 +86,6 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
|||||||
@Captor
|
@Captor
|
||||||
private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo>
|
private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo>
|
||||||
|
|
||||||
@Captor
|
|
||||||
private lateinit var booleanConsumer: ArgumentCaptor<Consumer<Boolean>>
|
|
||||||
|
|
||||||
@Captor
|
@Captor
|
||||||
private lateinit var controlLoadCallbackCaptor:
|
private lateinit var controlLoadCallbackCaptor:
|
||||||
ArgumentCaptor<ControlsBindingController.LoadCallback>
|
ArgumentCaptor<ControlsBindingController.LoadCallback>
|
||||||
@@ -936,4 +933,33 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
|||||||
verifyNoMoreInteractions(persistenceWrapper)
|
verifyNoMoreInteractions(persistenceWrapper)
|
||||||
verifyNoMoreInteractions(auxiliaryPersistenceWrapper)
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import org.junit.Before
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito.never
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
@SmallTest
|
@SmallTest
|
||||||
@@ -43,6 +45,8 @@ class AllModelTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
lateinit var pendingIntent: PendingIntent
|
lateinit var pendingIntent: PendingIntent
|
||||||
|
@Mock
|
||||||
|
lateinit var controlsModelCallback: ControlsModel.ControlsModelCallback
|
||||||
|
|
||||||
val idPrefix = "controlId"
|
val idPrefix = "controlId"
|
||||||
val favoritesIndices = listOf(7, 3, 1, 9)
|
val favoritesIndices = listOf(7, 3, 1, 9)
|
||||||
@@ -84,7 +88,7 @@ class AllModelTest : SysuiTestCase() {
|
|||||||
it in favoritesIndices
|
it in favoritesIndices
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
model = AllModel(controls, favoritesList, EMPTY_STRING)
|
model = AllModel(controls, favoritesList, EMPTY_STRING, controlsModelCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -93,28 +97,28 @@ class AllModelTest : SysuiTestCase() {
|
|||||||
// Zones are sorted by order of appearance, with empty at the end with special header.
|
// Zones are sorted by order of appearance, with empty at the end with special header.
|
||||||
val expected = listOf(
|
val expected = listOf(
|
||||||
ZoneNameWrapper("1"),
|
ZoneNameWrapper("1"),
|
||||||
ControlWrapper(controls[0]),
|
ControlStatusWrapper(controls[0]),
|
||||||
ControlWrapper(controls[3]),
|
ControlStatusWrapper(controls[3]),
|
||||||
ControlWrapper(controls[6]),
|
ControlStatusWrapper(controls[6]),
|
||||||
ControlWrapper(controls[9]),
|
ControlStatusWrapper(controls[9]),
|
||||||
ZoneNameWrapper("2"),
|
ZoneNameWrapper("2"),
|
||||||
ControlWrapper(controls[1]),
|
ControlStatusWrapper(controls[1]),
|
||||||
ControlWrapper(controls[4]),
|
ControlStatusWrapper(controls[4]),
|
||||||
ControlWrapper(controls[7]),
|
ControlStatusWrapper(controls[7]),
|
||||||
ZoneNameWrapper("0"),
|
ZoneNameWrapper("0"),
|
||||||
ControlWrapper(controls[2]),
|
ControlStatusWrapper(controls[2]),
|
||||||
ControlWrapper(controls[5]),
|
ControlStatusWrapper(controls[5]),
|
||||||
ControlWrapper(controls[8]),
|
ControlStatusWrapper(controls[8]),
|
||||||
ZoneNameWrapper(EMPTY_STRING),
|
ZoneNameWrapper(EMPTY_STRING),
|
||||||
ControlWrapper(controls[10]),
|
ControlStatusWrapper(controls[10]),
|
||||||
ControlWrapper(controls[11])
|
ControlStatusWrapper(controls[11])
|
||||||
)
|
)
|
||||||
expected.zip(model.elements).forEachIndexed { index, it ->
|
expected.zip(model.elements).forEachIndexed { index, it ->
|
||||||
assertEquals("Error in item at index $index", it.first, it.second)
|
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 &&
|
return controlInfo.controlId == control.controlId &&
|
||||||
controlInfo.controlTitle == control.title &&
|
controlInfo.controlTitle == control.title &&
|
||||||
controlInfo.controlSubtitle == control.subtitle &&
|
controlInfo.controlSubtitle == control.subtitle &&
|
||||||
@@ -124,10 +128,11 @@ class AllModelTest : SysuiTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun testAllEmpty_noHeader() {
|
fun testAllEmpty_noHeader() {
|
||||||
val selected_controls = listOf(controls[10], controls[11])
|
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(
|
val expected = listOf(
|
||||||
ControlWrapper(controls[10]),
|
ControlStatusWrapper(controls[10]),
|
||||||
ControlWrapper(controls[11])
|
ControlStatusWrapper(controls[11])
|
||||||
)
|
)
|
||||||
|
|
||||||
expected.zip(new_model.elements).forEachIndexed { index, it ->
|
expected.zip(new_model.elements).forEachIndexed { index, it ->
|
||||||
@@ -154,6 +159,8 @@ class AllModelTest : SysuiTestCase() {
|
|||||||
model.favorites.zip(expectedFavorites).forEach {
|
model.favorites.zip(expectedFavorites).forEach {
|
||||||
assertTrue(sameControl(it.first, it.second))
|
assertTrue(sameControl(it.first, it.second))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify(controlsModelCallback).onFirstChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -163,10 +170,12 @@ class AllModelTest : SysuiTestCase() {
|
|||||||
model.changeFavoriteStatus(id, true)
|
model.changeFavoriteStatus(id, true)
|
||||||
assertTrue(
|
assertTrue(
|
||||||
(model.elements.first {
|
(model.elements.first {
|
||||||
it is ControlWrapper && it.controlStatus.control.controlId == id
|
it is ControlStatusWrapper && it.controlStatus.control.controlId == id
|
||||||
} as ControlWrapper)
|
} as ControlStatusWrapper)
|
||||||
.controlStatus.favorite
|
.controlStatus.favorite
|
||||||
)
|
)
|
||||||
|
|
||||||
|
verify(controlsModelCallback).onFirstChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -180,6 +189,8 @@ class AllModelTest : SysuiTestCase() {
|
|||||||
model.favorites.zip(expectedFavorites).forEach {
|
model.favorites.zip(expectedFavorites).forEach {
|
||||||
assertTrue(sameControl(it.first, it.second))
|
assertTrue(sameControl(it.first, it.second))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify(controlsModelCallback, never()).onFirstChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -194,6 +205,8 @@ class AllModelTest : SysuiTestCase() {
|
|||||||
model.favorites.zip(expectedFavorites).forEach {
|
model.favorites.zip(expectedFavorites).forEach {
|
||||||
assertTrue(sameControl(it.first, it.second))
|
assertTrue(sameControl(it.first, it.second))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify(controlsModelCallback).onFirstChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -203,10 +216,12 @@ class AllModelTest : SysuiTestCase() {
|
|||||||
model.changeFavoriteStatus(id, false)
|
model.changeFavoriteStatus(id, false)
|
||||||
assertFalse(
|
assertFalse(
|
||||||
(model.elements.first {
|
(model.elements.first {
|
||||||
it is ControlWrapper && it.controlStatus.control.controlId == id
|
it is ControlStatusWrapper && it.controlStatus.control.controlId == id
|
||||||
} as ControlWrapper)
|
} as ControlStatusWrapper)
|
||||||
.controlStatus.favorite
|
.controlStatus.favorite
|
||||||
)
|
)
|
||||||
|
|
||||||
|
verify(controlsModelCallback).onFirstChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -219,5 +234,7 @@ class AllModelTest : SysuiTestCase() {
|
|||||||
model.favorites.zip(expectedFavorites).forEach {
|
model.favorites.zip(expectedFavorites).forEach {
|
||||||
assertTrue(sameControl(it.first, it.second))
|
assertTrue(sameControl(it.first, it.second))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify(controlsModelCallback, never()).onFirstChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user