Merge "Changes to favorite management screen"

This commit is contained in:
Fabian Kozynski
2020-02-10 20:55:16 +00:00
committed by Android (Google) Code Review
18 changed files with 1027 additions and 195 deletions

View File

@@ -646,16 +646,17 @@
android:label="Controls Providers"
android:theme="@style/Theme.ControlsManagement"
android:showForAllUsers="true"
android:clearTaskOnLaunch="true"
android:excludeFromRecents="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
android:visibleToInstantApps="true">
</activity>
<activity android:name=".controls.management.ControlsFavoritingActivity"
android:parentActivityName=".controls.management.ControlsProviderSelectorActivity"
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>

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:paddingTop="@dimen/controls_management_top_padding"
android:paddingStart="@dimen/controls_management_side_padding"
android:paddingEnd="@dimen/controls_management_side_padding" >
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/controls_title_size"
android:textAlignment="center" />
<TextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/controls_management_titles_margin"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAlignment="center" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/controls_management_list_margin" />
</LinearLayout>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Control.Title"
android:textColor="?android:attr/colorPrimary"
android:layout_marginStart="12dp"
android:layout_marginEnd="2dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp">
</TextView>

View File

@@ -72,8 +72,8 @@
<CheckBox
android:id="@+id/favorite"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:paddingTop="@dimen/controls_management_top_padding"
android:paddingStart="@dimen/controls_management_side_padding"
android:paddingEnd="@dimen/controls_management_side_padding" >
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/controls_title_size"
android:textAlignment="center" />
<TextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/controls_management_titles_margin"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAlignment="center" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginTop="@dimen/controls_management_list_margin">
<ViewStub
android:id="@+id/stub"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.core.widget.NestedScrollView>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="64dp">
<View
android:layout_width="match_parent"
android:layout_height="@dimen/controls_app_divider_height"
android:layout_gravity="center_horizontal|top"
android:background="?android:attr/listDivider" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="See other apps"
android:textAppearance="@style/TextAppearance.Control.Title"
android:textColor="?android:attr/colorPrimary"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<Button
android:id="@+id/done"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="Done"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<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"
>
</androidx.recyclerview.widget.RecyclerView>

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text_favorites"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="FAVORITES"
android:textAppearance="?android:attr/textAppearanceSmall"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/divider1"
app:layout_constraintTop_toTopOf="parent"
/>
<View
android:id="@+id/divider1"
android:layout_width="match_parent"
android:layout_height="@dimen/controls_app_divider_height"
android:layout_gravity="center_horizontal|top"
android:background="?android:attr/listDivider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/listFavorites"
app:layout_constraintTop_toBottomOf="@id/text_favorites"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listFavorites"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/controls_management_list_margin"
android:nestedScrollingEnabled="false"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/text_all"
app:layout_constraintTop_toBottomOf="@id/divider1"/>
<TextView
android:id="@+id/text_all"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/controls_management_list_margin"
android:text="ALL"
android:textAppearance="?android:attr/textAppearanceSmall"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/divider2"
app:layout_constraintTop_toBottomOf="@id/listFavorites"
/>
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="@dimen/controls_app_divider_height"
android:layout_gravity="center_horizontal|top"
android:background="?android:attr/listDivider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/listAll"
app:layout_constraintTop_toBottomOf="@id/text_all"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listAll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/controls_management_list_margin"
android:nestedScrollingEnabled="false"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider2"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,3 +1,18 @@
<!--
~ Copyright (C) 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

View File

@@ -20,6 +20,6 @@ import android.service.controls.Control
data class ControlStatus(
val control: Control,
val favorite: Boolean,
var favorite: Boolean,
val removed: Boolean = false
)

View File

@@ -26,10 +26,18 @@ interface ControlsController : UserAwareController {
val available: Boolean
fun getFavoriteControls(): List<ControlInfo>
fun loadForComponent(componentName: ComponentName, callback: (List<ControlStatus>) -> Unit)
fun loadForComponent(
componentName: ComponentName,
callback: (List<ControlStatus>, List<String>) -> Unit
)
fun subscribeToFavorites()
fun changeFavoriteStatus(controlInfo: ControlInfo, state: Boolean)
fun countFavoritesForComponent(componentName: ComponentName): Int = 0
fun replaceFavoritesForComponent(componentName: ComponentName, favorites: List<ControlInfo>)
fun getFavoritesForComponent(componentName: ComponentName): List<ControlInfo>
fun countFavoritesForComponent(componentName: ComponentName): Int
fun unsubscribe()
fun action(controlInfo: ControlInfo, action: ControlAction)
fun refreshStatus(componentName: ComponentName, control: Control)

View File

@@ -70,9 +70,10 @@ class ControlsControllerImpl @Inject constructor (
}
// Map of map: ComponentName -> (String -> ControlInfo).
// Only for current user
//
@GuardedBy("currentFavorites")
private val currentFavorites = ArrayMap<ComponentName, MutableMap<String, ControlInfo>>()
private val currentFavorites = ArrayMap<ComponentName, MutableList<ControlInfo>>()
.withDefault { mutableListOf() }
private var userChanging: Boolean = true
@@ -180,15 +181,14 @@ class ControlsControllerImpl @Inject constructor (
val infos = persistenceWrapper.readFavorites()
synchronized(currentFavorites) {
infos.forEach {
currentFavorites.getOrPut(it.component, { ArrayMap<String, ControlInfo>() })
.put(it.controlId, it)
currentFavorites.getOrPut(it.component, { mutableListOf() }).add(it)
}
}
}
override fun loadForComponent(
componentName: ComponentName,
callback: (List<ControlStatus>) -> Unit
callback: (List<ControlStatus>, List<String>) -> Unit
) {
if (!confirmAvailability()) {
if (userChanging) {
@@ -200,29 +200,34 @@ class ControlsControllerImpl @Inject constructor (
TimeUnit.MILLISECONDS
)
} else {
callback(emptyList())
callback(emptyList(), emptyList())
}
return
}
bindingController.bindAndLoad(componentName) {
synchronized(currentFavorites) {
val favoritesForComponentKeys: Set<String> =
currentFavorites.get(componentName)?.keys ?: emptySet()
val changed = updateFavoritesLocked(componentName, it)
val favoritesForComponentKeys: List<String> =
currentFavorites.getValue(componentName).map { it.controlId }
val changed = updateFavoritesLocked(componentName, it, favoritesForComponentKeys)
if (changed) {
persistenceWrapper.storeFavorites(favoritesAsListLocked())
}
val removed = findRemovedLocked(favoritesForComponentKeys, it)
callback(removed.map { currentFavorites.getValue(componentName).getValue(it) }
.map(::createRemovedStatus) +
it.map { ControlStatus(it, it.controlId in favoritesForComponentKeys) })
val removed = findRemovedLocked(favoritesForComponentKeys.toSet(), it)
val controlsWithFavorite =
it.map { ControlStatus(it, it.controlId in favoritesForComponentKeys) }
callback(
currentFavorites.getValue(componentName)
.filter { it.controlId in removed }
.map(::createRemovedStatus) + controlsWithFavorite,
favoritesForComponentKeys
)
}
}
}
private fun createRemovedStatus(controlInfo: ControlInfo): ControlStatus {
val intent = Intent(context, ControlsFavoritingActivity::class.java).apply {
putExtra(ControlsFavoritingActivity.EXTRA_COMPONENT, controlInfo.component)
putExtra(Intent.EXTRA_COMPONENT_NAME, controlInfo.component)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val pendingIntent = PendingIntent.getActivity(context,
@@ -243,17 +248,24 @@ class ControlsControllerImpl @Inject constructor (
}
@GuardedBy("currentFavorites")
private fun updateFavoritesLocked(componentName: ComponentName, list: List<Control>): Boolean {
val favorites = currentFavorites.get(componentName) ?: mutableMapOf()
val favoriteKeys = favorites.keys
private fun updateFavoritesLocked(
componentName: ComponentName,
list: List<Control>,
favoriteKeys: List<String>
): Boolean {
val favorites = currentFavorites.get(componentName) ?: mutableListOf()
if (favoriteKeys.isEmpty()) return false // early return
var changed = false
list.forEach {
if (it.controlId in favoriteKeys) {
val value = favorites.getValue(it.controlId)
if (value.controlTitle != it.title || value.deviceType != it.deviceType) {
favorites[it.controlId] = value.copy(controlTitle = it.title,
deviceType = it.deviceType)
list.forEach { control ->
if (control.controlId in favoriteKeys) {
val index = favorites.indexOfFirst { it.controlId == control.controlId }
val value = favorites[index]
if (value.controlTitle != control.title ||
value.deviceType != control.deviceType) {
favorites[index] = value.copy(
controlTitle = control.title,
deviceType = control.deviceType
)
changed = true
}
}
@@ -263,14 +275,14 @@ class ControlsControllerImpl @Inject constructor (
@GuardedBy("currentFavorites")
private fun favoritesAsListLocked(): List<ControlInfo> {
return currentFavorites.flatMap { it.value.values }
return currentFavorites.flatMap { it.value }
}
override fun subscribeToFavorites() {
if (!confirmAvailability()) return
// Make a copy of the favorites list
val favorites = synchronized(currentFavorites) {
currentFavorites.flatMap { it.value.values.toList() }
currentFavorites.flatMap { it.value }
}
bindingController.subscribe(favorites)
}
@@ -286,22 +298,19 @@ class ControlsControllerImpl @Inject constructor (
val listOfControls = synchronized(currentFavorites) {
if (state) {
if (controlInfo.component !in currentFavorites) {
currentFavorites.put(controlInfo.component, ArrayMap<String, ControlInfo>())
currentFavorites.put(controlInfo.component, mutableListOf())
changed = true
}
val controlsForComponent = currentFavorites.getValue(controlInfo.component)
if (controlInfo.controlId !in controlsForComponent) {
controlsForComponent.put(controlInfo.controlId, controlInfo)
if (controlsForComponent.firstOrNull {
it.controlId == controlInfo.controlId
} == null) {
controlsForComponent.add(controlInfo)
changed = true
} else {
if (controlsForComponent.getValue(controlInfo.controlId) != controlInfo) {
controlsForComponent.put(controlInfo.controlId, controlInfo)
changed = true
}
}
} else {
changed = currentFavorites.get(controlInfo.component)
?.remove(controlInfo.controlId) != null
?.remove(controlInfo) != null
}
favoritesAsListLocked()
}
@@ -310,6 +319,19 @@ class ControlsControllerImpl @Inject constructor (
}
}
override fun replaceFavoritesForComponent(
componentName: ComponentName,
favorites: List<ControlInfo>
) {
if (!confirmAvailability()) return
val filtered = favorites.filter { it.component == componentName }
val listOfControls = synchronized(currentFavorites) {
currentFavorites.put(componentName, filtered.toMutableList())
favoritesAsListLocked()
}
persistenceWrapper.storeFavorites(listOfControls)
}
override fun refreshStatus(componentName: ComponentName, control: Control) {
if (!confirmAvailability()) {
Log.d(TAG, "Controls not available")
@@ -317,7 +339,13 @@ class ControlsControllerImpl @Inject constructor (
}
executor.execute {
synchronized(currentFavorites) {
val changed = updateFavoritesLocked(componentName, listOf(control))
val favoriteKeysForComponent =
currentFavorites.get(componentName)?.map { it.controlId } ?: emptyList()
val changed = updateFavoritesLocked(
componentName,
listOf(control),
favoriteKeysForComponent
)
if (changed) {
persistenceWrapper.storeFavorites(favoritesAsListLocked())
}
@@ -361,6 +389,12 @@ class ControlsControllerImpl @Inject constructor (
}
}
override fun getFavoritesForComponent(componentName: ComponentName): List<ControlInfo> {
return synchronized(currentFavorites) {
currentFavorites.get(componentName) ?: emptyList()
}
}
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
pw.println("ControlsController state:")
pw.println(" Available: $available")
@@ -368,10 +402,8 @@ class ControlsControllerImpl @Inject constructor (
pw.println(" Current user: ${currentUser.identifier}")
pw.println(" Favorites:")
synchronized(currentFavorites) {
currentFavorites.forEach {
it.value.forEach {
pw.println(" ${it.value}")
}
favoritesAsListLocked().forEach {
pw.println(" ${ it }")
}
}
pw.println(bindingController.toString())

View File

@@ -29,6 +29,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.android.settingslib.applications.DefaultAppInfo
import com.android.settingslib.widget.CandidateInfo
import com.android.systemui.R
import java.text.Collator
import java.util.concurrent.Executor
/**
@@ -44,23 +45,27 @@ import java.util.concurrent.Executor
* @param onAppSelected a callback to indicate that an app has been selected in the list.
*/
class AppAdapter(
backgroundExecutor: Executor,
uiExecutor: Executor,
lifecycle: Lifecycle,
controlsListingController: ControlsListingController,
private val layoutInflater: LayoutInflater,
private val onAppSelected: (ComponentName?) -> Unit = {},
private val favoritesRenderer: FavoritesRenderer
private val favoritesRenderer: FavoritesRenderer,
private val resources: Resources
) : RecyclerView.Adapter<AppAdapter.Holder>() {
private var listOfServices = emptyList<CandidateInfo>()
private val callback = object : ControlsListingController.ControlsListingCallback {
override fun onServicesUpdated(list: List<CandidateInfo>) {
uiExecutor.execute {
listOfServices = list.sortedBy {
it.loadLabel().toString()
backgroundExecutor.execute {
val collator = Collator.getInstance(resources.getConfiguration().locale)
val localeComparator = compareBy<CandidateInfo, CharSequence>(collator) {
it.loadLabel()
}
notifyDataSetChanged()
listOfServices = list.sortedWith(localeComparator)
uiExecutor.execute(::notifyDataSetChanged)
}
}
}

View File

@@ -25,101 +25,150 @@ import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R
import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.controller.ControlInfo
import com.android.systemui.controls.ui.RenderInfo
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
* [changeFavoritesModel]. This allows for updating the model if there's a reload.
*
* @param layoutInflater an inflater for the views in the containing [RecyclerView]
* @param favoriteCallback a callback to be called when the favorite status of a [Control] is
* changed. The callback will take a [ControlInfo.Builder] that's
* pre-populated with the [Control] information and the new favorite
* status.
* @param onlyFavorites set to true to only display favorites instead of all controls
*/
class ControlAdapter(
private val layoutInflater: LayoutInflater,
private val favoriteCallback: (ControlInfo.Builder, Boolean) -> Unit
) : RecyclerView.Adapter<ControlAdapter.Holder>() {
private val onlyFavorites: Boolean = false
) : RecyclerView.Adapter<Holder>() {
var listOfControls = emptyList<ControlStatus>()
override fun onCreateViewHolder(parent: ViewGroup, i: Int): Holder {
return Holder(layoutInflater.inflate(R.layout.controls_base_item, parent, false).apply {
layoutParams.apply {
width = ViewGroup.LayoutParams.MATCH_PARENT
}
elevation = 15f
})
companion object {
private const val TYPE_ZONE = 0
private const val TYPE_CONTROL = 1
}
override fun getItemCount() = listOfControls.size
val spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (getItemViewType(position) == TYPE_ZONE) 2 else 1
}
}
var modelList: List<ElementWrapper> = emptyList()
private var favoritesModel: FavoriteModel? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
return when (viewType) {
TYPE_CONTROL -> {
ControlHolder(
layoutInflater.inflate(R.layout.controls_base_item, parent, false).apply {
layoutParams.apply {
width = ViewGroup.LayoutParams.MATCH_PARENT
}
elevation = 15f
},
{ id, favorite ->
favoritesModel?.changeFavoriteStatus(id, favorite)
})
}
TYPE_ZONE -> {
ZoneHolder(layoutInflater.inflate(R.layout.controls_zone_header, parent, false))
}
else -> throw IllegalStateException("Wrong viewType: $viewType")
}
}
fun changeFavoritesModel(favoritesModel: FavoriteModel) {
this.favoritesModel = favoritesModel
if (onlyFavorites) {
modelList = favoritesModel.favorites
} else {
modelList = favoritesModel.all
}
notifyDataSetChanged()
}
override fun getItemCount() = modelList.size
override fun onBindViewHolder(holder: Holder, index: Int) {
holder.bindData(listOfControls[index], favoriteCallback)
holder.bindData(modelList[index])
}
override fun getItemViewType(position: Int): Int {
return when (modelList[position]) {
is ZoneNameWrapper -> TYPE_ZONE
is ControlWrapper -> TYPE_CONTROL
}
}
}
/**
* Holder for binding views in the [RecyclerView]-
* @param view the [View] for this [Holder]
*/
sealed class Holder(view: View) : RecyclerView.ViewHolder(view) {
/**
* Holder for binding views in the [RecyclerView]-
* Bind the data from the model into the view
*/
class Holder(view: View) : RecyclerView.ViewHolder(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)
private val removed: TextView = itemView.requireViewById(R.id.status)
private val favorite: CheckBox = itemView.requireViewById<CheckBox>(R.id.favorite).apply {
visibility = View.VISIBLE
}
abstract fun bindData(wrapper: ElementWrapper)
}
/**
* Bind data to the view
* @param data information about the [Control]
* @param callback a callback to be called when the favorite status of the [Control] is
* changed. The callback will take a [ControlInfo.Builder] that's
* pre-populated with the [Control] information and the new favorite status.
*/
fun bindData(data: ControlStatus, callback: (ControlInfo.Builder, Boolean) -> Unit) {
val renderInfo = getRenderInfo(data.control.deviceType, data.favorite)
title.text = data.control.title
subtitle.text = data.control.subtitle
favorite.isChecked = data.favorite
removed.text = if (data.removed) "Removed" else ""
favorite.setOnClickListener {
val infoBuilder = ControlInfo.Builder().apply {
controlId = data.control.controlId
controlTitle = data.control.title
deviceType = data.control.deviceType
}
callback(infoBuilder, favorite.isChecked)
}
itemView.setOnClickListener {
favorite.performClick()
}
applyRenderInfo(renderInfo)
}
/**
* Holder for using with [ZoneNameWrapper] to display names of zones.
*/
private class ZoneHolder(view: View) : Holder(view) {
private val zone: TextView = itemView as TextView
private fun getRenderInfo(
@DeviceTypes.DeviceType deviceType: Int,
favorite: Boolean
): RenderInfo {
return RenderInfo.lookup(deviceType, favorite)
}
override fun bindData(wrapper: ElementWrapper) {
wrapper as ZoneNameWrapper
zone.text = wrapper.zoneName
}
}
private fun applyRenderInfo(ri: RenderInfo) {
val context = itemView.context
val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme())
icon.setImageIcon(Icon.createWithResource(context, ri.iconResourceId))
icon.setImageTintList(fg)
}
/**
* Holder for using with [ControlWrapper] 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) {
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)
private val removed: TextView = itemView.requireViewById(R.id.status)
private val favorite: CheckBox = itemView.requireViewById<CheckBox>(R.id.favorite).apply {
visibility = View.VISIBLE
}
fun setItems(list: List<ControlStatus>) {
listOfControls = list
notifyDataSetChanged()
override fun bindData(wrapper: ElementWrapper) {
wrapper as ControlWrapper
val data = wrapper.controlStatus
val renderInfo = getRenderInfo(data.control.deviceType)
title.text = data.control.title
subtitle.text = data.control.subtitle
favorite.isChecked = data.favorite
removed.text = if (data.removed) "Removed" else ""
favorite.setOnClickListener {
favoriteCallback(data.control.controlId, favorite.isChecked)
}
applyRenderInfo(renderInfo)
}
private fun getRenderInfo(
@DeviceTypes.DeviceType deviceType: Int
): RenderInfo {
return RenderInfo.lookup(deviceType, true)
}
private fun applyRenderInfo(ri: RenderInfo) {
val context = itemView.context
val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme())
icon.setImageIcon(Icon.createWithResource(context, ri.iconResourceId))
icon.setImageTintList(fg)
}
}

View File

@@ -18,10 +18,14 @@ 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.LayoutInflater
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
@@ -41,13 +45,36 @@ class ControlsFavoritingActivity @Inject constructor(
companion object {
private const val TAG = "ControlsFavoritingActivity"
const val EXTRA_APP = "extra_app_label"
const val EXTRA_COMPONENT = "extra_component"
}
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: ControlAdapter
private lateinit var recyclerViewAll: RecyclerView
private lateinit var adapterAll: ControlAdapter
private lateinit var recyclerViewFavorites: RecyclerView
private lateinit var adapterFavorites: ControlAdapter
private var component: ComponentName? = null
private var currentModel: FavoriteModel? = null
private var itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(
/* dragDirs */ ItemTouchHelper.UP
or ItemTouchHelper.DOWN
or ItemTouchHelper.LEFT
or ItemTouchHelper.RIGHT,
/* swipeDirs */0
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return currentModel?.onMoveItem(
viewHolder.adapterPosition, target.adapterPosition) != null
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
override fun isItemViewSwipeEnabled() = false
}
private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
private val startingUser = controller.currentUserId
@@ -62,41 +89,77 @@ class ControlsFavoritingActivity @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.controls_management)
requireViewById<ViewStub>(R.id.stub).apply {
layoutResource = R.layout.controls_management_favorites
inflate()
}
val app = intent.getCharSequenceExtra(EXTRA_APP)
component = intent.getParcelableExtra<ComponentName>(EXTRA_COMPONENT)
component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)
// If we have no component name, there's not much we can do.
val callback = component?.let {
{ infoBuilder: ControlInfo.Builder, status: Boolean ->
infoBuilder.componentName = it
controller.changeFavoriteStatus(infoBuilder.build(), status)
}
} ?: { _, _ -> Unit }
recyclerView = requireViewById(R.id.list)
adapter = ControlAdapter(LayoutInflater.from(applicationContext), callback)
recyclerView.adapter = adapter
recyclerView.layoutManager = GridLayoutManager(applicationContext, 2)
val margin = resources.getDimensionPixelSize(R.dimen.controls_card_margin)
recyclerView.addItemDecoration(MarginItemDecorator(margin, margin))
setUpRecyclerViews()
requireViewById<TextView>(R.id.title).text = app?.let { it }
?: resources.getText(R.string.controls_favorite_default_title)
requireViewById<TextView>(R.id.subtitle).text =
resources.getText(R.string.controls_favorite_subtitle)
currentUserTracker.startTracking()
}
requireViewById<Button>(R.id.done).setOnClickListener {
if (component == null) return@setOnClickListener
val favoritesForStorage = currentModel?.favorites?.map {
with(it.controlStatus.control) {
ControlInfo(component!!, controlId, title, deviceType)
}
}
if (favoritesForStorage != null) {
controller.replaceFavoritesForComponent(component!!, favoritesForStorage)
finishAffinity()
}
}
override fun onResume() {
super.onResume()
component?.let {
controller.loadForComponent(it) {
controller.loadForComponent(it) { allControls, favoriteKeys ->
executor.execute {
adapter.setItems(it)
val favoriteModel = FavoriteModel(
allControls,
favoriteKeys,
allAdapter = adapterAll,
favoritesAdapter = adapterFavorites)
adapterAll.changeFavoritesModel(favoriteModel)
adapterFavorites.changeFavoritesModel(favoriteModel)
currentModel = favoriteModel
}
}
}
currentUserTracker.startTracking()
}
private fun setUpRecyclerViews() {
val margin = resources.getDimensionPixelSize(R.dimen.controls_card_margin)
val itemDecorator = MarginItemDecorator(margin, margin)
val layoutInflater = LayoutInflater.from(applicationContext)
adapterAll = ControlAdapter(layoutInflater)
recyclerViewAll = requireViewById<RecyclerView>(R.id.listAll).apply {
adapter = adapterAll
layoutManager = GridLayoutManager(applicationContext, 2).apply {
spanSizeLookup = adapterAll.spanSizeLookup
}
addItemDecoration(itemDecorator)
}
adapterFavorites = ControlAdapter(layoutInflater, true)
recyclerViewFavorites = requireViewById<RecyclerView>(R.id.listFavorites).apply {
layoutManager = GridLayoutManager(applicationContext, 2)
adapter = adapterFavorites
addItemDecoration(itemDecorator)
}
ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(recyclerViewFavorites)
}
override fun onDestroy() {
currentUserTracker.stopTracking()
super.onDestroy()
}
}

View File

@@ -20,6 +20,7 @@ import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewStub
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -63,11 +64,21 @@ class ControlsProviderSelectorActivity @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.controls_management)
requireViewById<ViewStub>(R.id.stub).apply {
layoutResource = R.layout.controls_management_apps
inflate()
}
recyclerView = requireViewById(R.id.list)
recyclerView.adapter = AppAdapter(executor, lifecycle, listingController,
LayoutInflater.from(this), ::launchFavoritingActivity,
FavoritesRenderer(resources, controlsController::countFavoritesForComponent))
recyclerView.adapter = AppAdapter(
backExecutor,
executor,
lifecycle,
listingController,
LayoutInflater.from(this),
::launchFavoritingActivity,
FavoritesRenderer(resources, controlsController::countFavoritesForComponent),
resources)
recyclerView.layoutManager = LinearLayoutManager(applicationContext)
requireViewById<TextView>(R.id.title).text =
@@ -89,11 +100,16 @@ class ControlsProviderSelectorActivity @Inject constructor(
.apply {
putExtra(ControlsFavoritingActivity.EXTRA_APP,
listingController.getAppLabel(it))
putExtra(ControlsFavoritingActivity.EXTRA_COMPONENT, it)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
putExtra(Intent.EXTRA_COMPONENT_NAME, it)
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
}
startActivity(intent)
}
}
}
override fun onDestroy() {
currentUserTracker.stopTracking()
super.onDestroy()
}
}

View File

@@ -0,0 +1,153 @@
/*
* 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())
}
}
/**
* Wrapper classes for the different types of elements shown in the [RecyclerView]s in
* [ControlsFavoritingActivity].
*/
sealed class ElementWrapper
data class ZoneNameWrapper(val zoneName: CharSequence) : ElementWrapper()
data class ControlWrapper(val controlStatus: ControlStatus) : ElementWrapper()

View File

@@ -39,6 +39,7 @@ import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@@ -225,7 +226,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2)
val control = builderFromInfo(newControlInfo).build()
controller.loadForComponent(TEST_COMPONENT) {}
controller.loadForComponent(TEST_COMPONENT) { _, _ -> Unit }
reset(persistenceWrapper)
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
@@ -293,11 +294,13 @@ class ControlsControllerImplTest : SysuiTestCase() {
var loaded = false
val control = builderFromInfo(TEST_CONTROL_INFO).build()
controller.loadForComponent(TEST_COMPONENT) {
controller.loadForComponent(TEST_COMPONENT) { controls, favorites ->
loaded = true
assertEquals(1, it.size)
val controlStatus = it[0]
assertEquals(1, controls.size)
val controlStatus = controls[0]
assertEquals(ControlStatus(control, false), controlStatus)
assertTrue(favorites.isEmpty())
}
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
@@ -315,14 +318,17 @@ class ControlsControllerImplTest : SysuiTestCase() {
val control2 = builderFromInfo(TEST_CONTROL_INFO_2).build()
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.loadForComponent(TEST_COMPONENT) {
controller.loadForComponent(TEST_COMPONENT) { controls, favorites ->
loaded = true
assertEquals(2, it.size)
val controlStatus = it.first { it.control.controlId == TEST_CONTROL_ID }
assertEquals(2, controls.size)
val controlStatus = controls.first { it.control.controlId == TEST_CONTROL_ID }
assertEquals(ControlStatus(control, true), controlStatus)
val controlStatus2 = it.first { it.control.controlId == TEST_CONTROL_ID_2 }
val controlStatus2 = controls.first { it.control.controlId == TEST_CONTROL_ID_2 }
assertEquals(ControlStatus(control2, false), controlStatus2)
assertEquals(1, favorites.size)
assertEquals(TEST_CONTROL_ID, favorites[0])
}
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
@@ -338,13 +344,16 @@ class ControlsControllerImplTest : SysuiTestCase() {
var loaded = false
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.loadForComponent(TEST_COMPONENT) {
controller.loadForComponent(TEST_COMPONENT) { controls, favorites ->
loaded = true
assertEquals(1, it.size)
val controlStatus = it[0]
assertEquals(1, controls.size)
val controlStatus = controls[0]
assertEquals(TEST_CONTROL_ID, controlStatus.control.controlId)
assertTrue(controlStatus.favorite)
assertTrue(controlStatus.removed)
assertEquals(1, favorites.size)
assertEquals(TEST_CONTROL_ID, favorites[0])
}
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
@@ -361,7 +370,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2)
val control = builderFromInfo(newControlInfo).build()
controller.loadForComponent(TEST_COMPONENT) {}
controller.loadForComponent(TEST_COMPONENT) { _, _ -> Unit }
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
capture(controlLoadCallbackCaptor))
@@ -483,4 +492,81 @@ class ControlsControllerImplTest : SysuiTestCase() {
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT_2))
}
@Test
fun testGetFavoritesForComponent() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
assertEquals(listOf(TEST_CONTROL_INFO), controller.getFavoritesForComponent(TEST_COMPONENT))
}
@Test
fun testGetFavoritesForComponent_otherComponent() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO_2, true)
assertTrue(controller.getFavoritesForComponent(TEST_COMPONENT).isEmpty())
}
@Test
fun testGetFavoritesForComponent_multipleInOrder() {
val controlInfo = ControlInfo(TEST_COMPONENT, "id", "title", 0)
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.changeFavoriteStatus(controlInfo, true)
assertEquals(listOf(TEST_CONTROL_INFO, controlInfo),
controller.getFavoritesForComponent(TEST_COMPONENT))
controller.clearFavorites()
controller.changeFavoriteStatus(controlInfo, true)
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
assertEquals(listOf(controlInfo, TEST_CONTROL_INFO),
controller.getFavoritesForComponent(TEST_COMPONENT))
}
@Test
fun testReplaceFavoritesForComponent_noFavorites() {
controller.replaceFavoritesForComponent(TEST_COMPONENT, listOf(TEST_CONTROL_INFO))
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOf(TEST_CONTROL_INFO), controller.getFavoritesForComponent(TEST_COMPONENT))
}
@Test
fun testReplaceFavoritesForComponent_differentComponentsAreFilteredOut() {
controller.replaceFavoritesForComponent(TEST_COMPONENT,
listOf(TEST_CONTROL_INFO, TEST_CONTROL_INFO_2))
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOf(TEST_CONTROL_INFO), controller.getFavoritesForComponent(TEST_COMPONENT))
}
@Test
fun testReplaceFavoritesForComponent_oldFavoritesRemoved() {
val controlInfo = ControlInfo(TEST_COMPONENT, "id", "title", 0)
assertNotEquals(TEST_CONTROL_INFO, controlInfo)
controller.changeFavoriteStatus(controlInfo, true)
controller.replaceFavoritesForComponent(TEST_COMPONENT, listOf(TEST_CONTROL_INFO))
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOf(TEST_CONTROL_INFO), controller.getFavoritesForComponent(TEST_COMPONENT))
}
@Test
fun testReplaceFavoritesForComponent_favoritesInOrder() {
val controlInfo = ControlInfo(TEST_COMPONENT, "id", "title", 0)
val listOrder1 = listOf(TEST_CONTROL_INFO, controlInfo)
controller.replaceFavoritesForComponent(TEST_COMPONENT, listOrder1)
assertEquals(2, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOrder1, controller.getFavoritesForComponent(TEST_COMPONENT))
val listOrder2 = listOf(controlInfo, TEST_CONTROL_INFO)
controller.replaceFavoritesForComponent(TEST_COMPONENT, listOrder2)
assertEquals(2, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOrder2, controller.getFavoritesForComponent(TEST_COMPONENT))
}
}

View File

@@ -0,0 +1,198 @@
/*
* 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.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(),
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)
}
}