Merge changes I77671bd4,I22021266 into rvc-dev

* changes:
  Controls UI - Structure switching
  Controls - Structure persistence
This commit is contained in:
Matt Pietal
2020-03-03 18:32:41 +00:00
committed by Android (Google) Code Review
24 changed files with 961 additions and 792 deletions

View File

@@ -0,0 +1,23 @@
<?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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="@color/control_secondary_text">
<solid android:color="#33000000" />
<size
android:height="1dp"
android:width="1dp" />
</shape>

View File

@@ -1,24 +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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View File

@@ -0,0 +1,50 @@
<!--
~ 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"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<Space
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="1dp" />
<ImageView
android:id="@+id/app_icon"
android:layout_gravity="center"
android:layout_width="34dp"
android:layout_height="24dp"
android:layout_marginEnd="10dp" />
<TextView
android:id="@+id/controls_spinner_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:layout_gravity="center"
android:textSize="25sp"
android:textColor="@color/control_secondary_text"
android:fontFamily="@*android:string/config_headlineFontFamily" />
<Space
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="1dp" />
</LinearLayout>

View File

@@ -14,44 +14,47 @@
~ limitations under the License.
-->
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:id="@+id/controls_header"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="20dp">
android:paddingTop="12dp">
<TextView
android:text="@string/quick_controls_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:gravity="center"
android:textSize="25sp"
android:textColor="@*android:color/foreground_material_dark"
android:fontFamily="@*android:string/config_headlineFontFamily"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Space
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="1dp" />
<ImageView
android:id="@+id/controls_more"
android:src="@drawable/ic_more_vert"
android:layout_width="34dp"
android:id="@+id/app_icon"
android:layout_gravity="center"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="10dp"
android:tint="@*android:color/foreground_material_dark"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:layout_marginEnd="10dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
style="@style/Control.Spinner.Header"
android:clickable="false"
android:id="@+id/app_or_structure_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:layout_gravity="center"
android:ellipsize="end" />
<Space
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="1dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/global_actions_controls_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
android:orientation="vertical"
android:paddingTop="20dp" />
</merge>

View File

@@ -1217,6 +1217,7 @@
<!-- Home Controls -->
<dimen name="control_spacing">4dp</dimen>
<dimen name="control_list_divider">1dp</dimen>
<dimen name="control_corner_radius">15dp</dimen>
<dimen name="control_height">100dp</dimen>
<dimen name="control_padding">15dp</dimen>

View File

@@ -656,6 +656,12 @@
<item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
</style>
<style name="Control.Spinner.Header" parent="@*android:style/Widget.DeviceDefault.Spinner.DropDown">
<item name="android:textSize">25sp</item>
<item name="android:textColor">@color/control_primary_text</item>
<item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
</style>
<style name="TextAppearance.Control.Status">
<item name="android:textSize">12sp</item>
<item name="android:textColor">@color/control_primary_text</item>
@@ -669,5 +675,8 @@
<item name="android:textSize">12sp</item>
<item name="android:textColor">@color/control_secondary_text</item>
</style>
<style name="Control.ListPopupWindow" parent="@android:style/Widget.ListPopupWindow">
<item name="android:overlapAnchor">true</item>
</style>
</resources>

View File

@@ -16,59 +16,27 @@
package com.android.systemui.controls.controller
import android.content.ComponentName
import android.service.controls.DeviceTypes
import android.util.Log
/**
* Stores basic information about a [Control] to persist and keep track of favorites.
*
* The identifier of this [Control] is the combination of [component] and [controlId]. The other
* two fields are there for persistence. In this way, basic information can be shown to the user
* The identifier of this [Control] is the [controlId], and is only unique per app. The other
* fields are there for persistence. In this way, basic information can be shown to the user
* before the service has to report on the status.
*
* @property component the name of the component that provides the [Control].
* @property controlId unique (for the given [component]) identifier for this [Control].
* @property controlId unique identifier for this [Control].
* @property controlTitle last title reported for this [Control].
* @property deviceType last reported type for this [Control].
*/
data class ControlInfo(
val component: ComponentName,
val controlId: String,
val controlTitle: CharSequence,
@DeviceTypes.DeviceType val deviceType: Int
) {
companion object {
private const val TAG = "ControlInfo"
private const val SEPARATOR = ":"
/**
* Creates a [ControlInfo] from a [SEPARATOR] separated list of fields.
*
* @param separator fields of a [ControlInfo] separated by [SEPARATOR]
* @return a [ControlInfo] or `null` if there was an error.
* @see [ControlInfo.toString]
*/
fun createFromString(string: String): ControlInfo? {
val parts = string.split(SEPARATOR)
val component = ComponentName.unflattenFromString(parts[0])
if (parts.size != 4 || component == null) {
Log.e(TAG, "Cannot parse ControlInfo from $string")
return null
}
val type = try {
parts[3].toInt()
} catch (e: Exception) {
Log.e(TAG, "Cannot parse deviceType from ${parts[3]}")
return null
}
return ControlInfo(
component,
parts[1],
parts[2],
if (DeviceTypes.validDeviceType(type)) type else DeviceTypes.TYPE_UNKNOWN)
}
}
/**
@@ -77,16 +45,14 @@ data class ControlInfo(
* @return a [String] representation of `this`
*/
override fun toString(): String {
return component.flattenToString() +
"$SEPARATOR$controlId$SEPARATOR$controlTitle$SEPARATOR$deviceType"
return "$SEPARATOR$controlId$SEPARATOR$controlTitle$SEPARATOR$deviceType"
}
class Builder {
lateinit var componentName: ComponentName
lateinit var controlId: String
lateinit var controlTitle: CharSequence
var deviceType: Int = DeviceTypes.TYPE_UNKNOWN
fun build() = ControlInfo(componentName, controlId, controlTitle, deviceType)
fun build() = ControlInfo(controlId, controlTitle, deviceType)
}
}
}

View File

@@ -42,30 +42,27 @@ interface ControlsBindingController : UserAwareController {
fun bindAndLoad(component: ComponentName, callback: LoadCallback)
/**
* Request to bind to the given services.
* Request to bind to the given service.
*
* @param components a list of [ComponentName] of the services to bind
* @param component The [ComponentName] of the service to bind
*/
fun bindServices(components: List<ComponentName>)
fun bindService(component: ComponentName)
/**
* Send a subscribe message to retrieve status of a set of controls.
*
* The controls passed do not have to belong to a single [ControlsProviderService]. The
* corresponding service [ComponentName] is associated with each control.
*
* @param controls a list of controls with corresponding [ComponentName] to request status
* update
* @param structureInfo structure containing the controls to update
*/
fun subscribe(controls: List<ControlInfo>)
fun subscribe(structureInfo: StructureInfo)
/**
* Send an action performed on a [Control].
*
* @param controlInfo information about the actioned control, including the [ComponentName]
* @param componentName name of the component
* @param controlInfo information about the actioned control
* @param action the action performed on the control
*/
fun action(controlInfo: ControlInfo, action: ControlAction)
fun action(componentName: ComponentName, controlInfo: ControlInfo, action: ControlAction)
/**
* Unsubscribe from all services to stop status updates.
@@ -91,4 +88,4 @@ interface ControlsBindingController : UserAwareController {
*/
fun error(message: String)
}
}
}

View File

@@ -26,9 +26,7 @@ import android.service.controls.IControlsActionCallback
import android.service.controls.IControlsSubscriber
import android.service.controls.IControlsSubscription
import android.service.controls.actions.ControlAction
import android.util.ArrayMap
import android.util.Log
import com.android.internal.annotations.GuardedBy
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.util.concurrency.DelayableExecutor
@@ -56,12 +54,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
override val currentUserId: Int
get() = currentUser.identifier
@GuardedBy("componentMap")
private val tokenMap: MutableMap<IBinder, ControlsProviderLifecycleManager> =
ArrayMap<IBinder, ControlsProviderLifecycleManager>()
@GuardedBy("componentMap")
private val componentMap: MutableMap<Key, ControlsProviderLifecycleManager> =
ArrayMap<Key, ControlsProviderLifecycleManager>()
private var currentProvider: ControlsProviderLifecycleManager? = null
private val actionCallbackService = object : IControlsActionCallback.Stub() {
override fun accept(
@@ -108,92 +101,70 @@ open class ControlsBindingControllerImpl @Inject constructor(
}
private fun retrieveLifecycleManager(component: ComponentName):
ControlsProviderLifecycleManager {
synchronized(componentMap) {
val provider = componentMap.getOrPut(Key(component, currentUser)) {
createProviderManager(component)
}
tokenMap.putIfAbsent(provider.token, provider)
return provider
ControlsProviderLifecycleManager? {
if (currentProvider != null && currentProvider?.componentName != component) {
unbind()
}
if (currentProvider == null) {
currentProvider = createProviderManager(component)
}
return currentProvider
}
override fun bindAndLoad(
component: ComponentName,
callback: ControlsBindingController.LoadCallback
) {
val provider = retrieveLifecycleManager(component)
provider.maybeBindAndLoad(LoadSubscriber(callback))
retrieveLifecycleManager(component)?.maybeBindAndLoad(LoadSubscriber(callback))
}
override fun subscribe(controls: List<ControlInfo>) {
val controlsByComponentName = controls.groupBy { it.component }
override fun subscribe(structureInfo: StructureInfo) {
if (refreshing.compareAndSet(false, true)) {
controlsByComponentName.forEach {
val provider = retrieveLifecycleManager(it.key)
backgroundExecutor.execute {
provider.maybeBindAndSubscribe(it.value.map { it.controlId })
}
}
}
// Unbind unneeded providers
val providersWithFavorites = controlsByComponentName.keys
synchronized(componentMap) {
componentMap.forEach {
if (it.key.component !in providersWithFavorites) {
backgroundExecutor.execute { it.value.unbindService() }
}
}
val provider = retrieveLifecycleManager(structureInfo.componentName)
provider?.maybeBindAndSubscribe(structureInfo.controls.map { it.controlId })
}
}
override fun unsubscribe() {
if (refreshing.compareAndSet(true, false)) {
val providers = synchronized(componentMap) {
componentMap.values.toList()
}
providers.forEach {
backgroundExecutor.execute { it.unsubscribe() }
}
currentProvider?.unsubscribe()
}
}
override fun action(controlInfo: ControlInfo, action: ControlAction) {
val provider = retrieveLifecycleManager(controlInfo.component)
provider.maybeBindAndSendAction(controlInfo.controlId, action)
override fun action(
componentName: ComponentName,
controlInfo: ControlInfo,
action: ControlAction
) {
retrieveLifecycleManager(componentName)
?.maybeBindAndSendAction(controlInfo.controlId, action)
}
override fun bindServices(components: List<ComponentName>) {
components.forEach {
val provider = retrieveLifecycleManager(it)
backgroundExecutor.execute { provider.bindService() }
}
override fun bindService(component: ComponentName) {
retrieveLifecycleManager(component)?.bindService()
}
override fun changeUser(newUser: UserHandle) {
if (newUser == currentUser) return
synchronized(componentMap) {
unbindAllProvidersLocked() // unbind all providers from the old user
}
unbind()
refreshing.set(false)
currentUser = newUser
}
private fun unbindAllProvidersLocked() {
componentMap.values.forEach {
if (it.user == currentUser) {
it.unbindService()
}
}
private fun unbind() {
currentProvider?.unbindService()
currentProvider = null
}
override fun onComponentRemoved(componentName: ComponentName) {
backgroundExecutor.execute {
synchronized(componentMap) {
val removed = componentMap.remove(Key(componentName, currentUser))
removed?.let {
it.unbindService()
tokenMap.remove(it.token)
currentProvider?.let {
if (it.componentName == componentName) {
unbind()
}
}
}
@@ -203,20 +174,31 @@ open class ControlsBindingControllerImpl @Inject constructor(
return StringBuilder(" ControlsBindingController:\n").apply {
append(" refreshing=${refreshing.get()}\n")
append(" currentUser=$currentUser\n")
append(" Providers:\n")
synchronized(componentMap) {
componentMap.values.forEach {
append(" $it\n")
}
}
append(" Providers=$currentProvider\n")
}.toString()
}
private abstract inner class CallbackRunnable(val token: IBinder) : Runnable {
protected val provider: ControlsProviderLifecycleManager? =
synchronized(componentMap) {
tokenMap.get(token)
}
protected val provider: ControlsProviderLifecycleManager? = currentProvider
override fun run() {
if (provider == null) {
Log.e(TAG, "No current provider set")
return
}
if (provider.user != currentUser) {
Log.e(TAG, "User ${provider.user} is not current user")
return
}
if (token != provider.token) {
Log.e(TAG, "Provider for token:$token does not exist anymore")
return
}
doRun()
}
abstract fun doRun()
}
private inner class OnLoadRunnable(
@@ -224,23 +206,9 @@ open class ControlsBindingControllerImpl @Inject constructor(
val list: List<Control>,
val callback: ControlsBindingController.LoadCallback
) : CallbackRunnable(token) {
override fun run() {
if (provider == null) {
Log.e(TAG, "No provider found for token:$token")
return
}
if (provider.user != currentUser) {
Log.e(TAG, "User ${provider.user} is not current user")
return
}
synchronized(componentMap) {
if (token !in tokenMap.keys) {
Log.e(TAG, "Provider for token:$token does not exist anymore")
return
}
}
override fun doRun() {
callback.accept(list)
provider.unbindService()
provider?.unbindService()
}
}
@@ -248,14 +216,11 @@ open class ControlsBindingControllerImpl @Inject constructor(
token: IBinder,
val control: Control
) : CallbackRunnable(token) {
override fun run() {
override fun doRun() {
if (!refreshing.get()) {
Log.d(TAG, "onRefresh outside of window from:${provider?.componentName}")
}
if (provider?.user != currentUser) {
Log.e(TAG, "User ${provider?.user} is not current user")
return
}
provider?.let {
lazyController.get().refreshStatus(it.componentName, control)
}
@@ -266,7 +231,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
token: IBinder,
val subscription: IControlsSubscription
) : CallbackRunnable(token) {
override fun run() {
override fun doRun() {
if (!refreshing.get()) {
Log.d(TAG, "onRefresh outside of window from '${provider?.componentName}'")
}
@@ -279,7 +244,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
private inner class OnCompleteRunnable(
token: IBinder
) : CallbackRunnable(token) {
override fun run() {
override fun doRun() {
provider?.let {
Log.i(TAG, "onComplete receive from '${it.componentName}'")
}
@@ -290,7 +255,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
token: IBinder,
val error: String
) : CallbackRunnable(token) {
override fun run() {
override fun doRun() {
provider?.let {
Log.e(TAG, "onError receive from '${it.componentName}': $error")
}
@@ -302,11 +267,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
val controlId: String,
@ControlAction.ResponseResult val response: Int
) : CallbackRunnable(token) {
override fun run() {
if (provider?.user != currentUser) {
Log.e(TAG, "User ${provider?.user} is not current user")
return
}
override fun doRun() {
provider?.let {
lazyController.get().onActionResponse(it.componentName, controlId, response)
}
@@ -318,7 +279,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
val error: String,
val callback: ControlsBindingController.LoadCallback
) : CallbackRunnable(token) {
override fun run() {
override fun doRun() {
callback.error(error)
provider?.let {
Log.e(TAG, "onError receive from '${it.componentName}': $error")

View File

@@ -59,14 +59,15 @@ interface ControlsController : UserAwareController {
)
/**
* Request to subscribe for all favorite controls.
* Request to subscribe for favorited controls per structure
*
* @param structureInfo structure to limit the subscription to
* @see [ControlsBindingController.subscribe]
*/
fun subscribeToFavorites()
fun subscribeToFavorites(structureInfo: StructureInfo)
/**
* Request to unsubscribe to all providers.
* Request to unsubscribe to the current provider.
*
* @see [ControlsBindingController.unsubscribe]
*/
@@ -75,11 +76,12 @@ interface ControlsController : UserAwareController {
/**
* Notify a [ControlsProviderService] that an action has been performed on a [Control].
*
* @param componentName the name of the service that provides the [Control]
* @param controlInfo information of the [Control] receiving the action
* @param action action performed on the [Control]
* @see [ControlsBindingController.action]
*/
fun action(controlInfo: ControlInfo, action: ControlAction)
fun action(componentName: ComponentName, controlInfo: ControlInfo, action: ControlAction)
/**
* Refresh the status of a [Control] with information provided from the service.
@@ -107,48 +109,29 @@ interface ControlsController : UserAwareController {
// FAVORITE MANAGEMENT
/**
* Get a list of all favorite controls.
* Get all the favorites.
*
* @return a list of [ControlInfo] with persistent information about the controls, including
* their corresponding [ComponentName].
* @return a list of the structures that have at least one favorited control
*/
fun getFavoriteControls(): List<ControlInfo>
fun getFavorites(): List<StructureInfo>
/**
* Get all the favorites for a given component.
*
* @param componentName the name of the component of the [ControlsProviderService] with
* which to filter the favorites.
* @return a list of the favorite controls for the given service. All the elements of the list
* will have the same [ControlInfo.component] matching the one requested.
* @param componentName the name of the service that provides the [Control]
* @return a list of the structures that have at least one favorited control
*/
fun getFavoritesForComponent(componentName: ComponentName): List<ControlInfo>
fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo>
/**
* Replaces the favorites for the given component.
* Replaces the favorites for the given structure.
*
* Calling this method will eliminate the previous selection of favorites and replace it with a
* new one.
*
* @param componentName The name of the component for the [ControlsProviderService]
* @param favorites a list of [ControlInfo] to replace the previous favorites.
* @param structureInfo common structure for all of the favorited controls
*/
fun replaceFavoritesForComponent(componentName: ComponentName, favorites: List<ControlInfo>)
/**
* Change the favorite status of a single [Control].
*
* If the control is added to favorites, it will be added to the end of the list for that
* particular component. Matching for removing the control will be done based on
* [ControlInfo.component] and [ControlInfo.controlId].
*
* Trying to add an already favorite control or trying to remove one that is not a favorite is
* a no-op.
*
* @param controlInfo persistent information about the [Control].
* @param state `true` to add to favorites and `false` to remove.
*/
fun changeFavoriteStatus(controlInfo: ControlInfo, state: Boolean)
fun replaceFavoritesForStructure(structureInfo: StructureInfo)
/**
* Return the number of favorites for a given component.
@@ -160,14 +143,6 @@ interface ControlsController : UserAwareController {
*/
fun countFavoritesForComponent(componentName: ComponentName): Int
/**
* Clears the list of all favorites.
*
* To clear the list of favorites for a given service, call [replaceFavoritesForComponent] with
* an empty list.
*/
fun clearFavorites()
/**
* Interface for structure to pass data to [ControlsFavoritingActivity].
*/

View File

@@ -31,9 +31,7 @@ import android.os.UserHandle
import android.provider.Settings
import android.service.controls.Control
import android.service.controls.actions.ControlAction
import android.util.ArrayMap
import android.util.Log
import com.android.internal.annotations.GuardedBy
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -72,12 +70,6 @@ class ControlsControllerImpl @Inject constructor (
private const val DEFAULT_ENABLED = 1
}
// Map of map: ComponentName -> (String -> ControlInfo).
//
@GuardedBy("currentFavorites")
private val currentFavorites = ArrayMap<ComponentName, MutableList<ControlInfo>>()
.withDefault { mutableListOf() }
private var userChanging: Boolean = true
private var currentUser = UserHandle.of(ActivityManager.getCurrentUser())
@@ -109,12 +101,7 @@ class ControlsControllerImpl @Inject constructor (
persistenceWrapper.changeFile(fileName)
available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
DEFAULT_ENABLED, newUser.identifier) != 0
synchronized(currentFavorites) {
currentFavorites.clear()
}
if (available) {
loadFavorites()
}
resetFavorites(available)
bindingController.changeUser(newUser)
listingController.changeUser(newUser)
userChanging = false
@@ -146,12 +133,7 @@ class ControlsControllerImpl @Inject constructor (
}
available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
DEFAULT_ENABLED, currentUserId) != 0
synchronized(currentFavorites) {
currentFavorites.clear()
}
if (available) {
loadFavorites()
}
resetFavorites(available)
}
}
@@ -163,21 +145,23 @@ class ControlsControllerImpl @Inject constructor (
* If some component has been removed, the new set of favorites will also be saved.
*/
private val listingCallback = object : ControlsListingController.ControlsListingCallback {
override fun onServicesUpdated(candidates: List<ControlsServiceInfo>) {
override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
executor.execute {
val candidateComponents = candidates.map(ControlsServiceInfo::componentName)
synchronized(currentFavorites) {
val components = currentFavorites.keys.toSet() // create a copy
components.forEach {
if (it !in candidateComponents) {
currentFavorites.remove(it)
bindingController.onComponentRemoved(it)
}
}
// Check if something has been removed, if so, store the new list
if (components.size > currentFavorites.size) {
persistenceWrapper.storeFavorites(favoritesAsListLocked())
}
val serviceInfoSet = serviceInfos.map(ControlsServiceInfo::componentName).toSet()
val favoriteComponentSet = Favorites.getAllStructures().map {
it.componentName
}.toSet()
var changed = false
favoriteComponentSet.subtract(serviceInfoSet).forEach {
changed = true
Favorites.removeStructures(it)
bindingController.onComponentRemoved(it)
}
// Check if something has been removed, if so, store the new list
if (changed) {
persistenceWrapper.storeFavorites(Favorites.getAllStructures())
}
}
}
@@ -185,9 +169,7 @@ class ControlsControllerImpl @Inject constructor (
init {
dumpManager.registerDumpable(javaClass.name, this)
if (available) {
loadFavorites()
}
resetFavorites(available)
userChanging = false
broadcastDispatcher.registerReceiver(
userSwitchReceiver,
@@ -198,6 +180,15 @@ class ControlsControllerImpl @Inject constructor (
contentResolver.registerContentObserver(URI, false, settingObserver, UserHandle.USER_ALL)
}
private fun resetFavorites(shouldLoad: Boolean) {
Favorites.clear()
if (shouldLoad) {
Favorites.load(persistenceWrapper.readFavorites())
listingController.addCallback(listingCallback)
}
}
private fun confirmAvailability(): Boolean {
if (userChanging) {
Log.w(TAG, "Controls not available while user is changing")
@@ -210,16 +201,6 @@ class ControlsControllerImpl @Inject constructor (
return true
}
private fun loadFavorites() {
val infos = persistenceWrapper.readFavorites()
synchronized(currentFavorites) {
infos.forEach {
currentFavorites.getOrPut(it.component, { mutableListOf() }).add(it)
}
}
listingController.addCallback(listingCallback)
}
override fun loadForComponent(
componentName: ComponentName,
dataCallback: Consumer<ControlsController.LoadData>
@@ -242,41 +223,41 @@ class ControlsControllerImpl @Inject constructor (
componentName,
object : ControlsBindingController.LoadCallback {
override fun accept(controls: List<Control>) {
val loadData = synchronized(currentFavorites) {
val favoritesForComponentKeys: List<String> =
currentFavorites.getValue(componentName).map { it.controlId }
val changed = updateFavoritesLocked(componentName, controls,
favoritesForComponentKeys)
executor.execute {
val favoritesForComponentKeys = Favorites
.getControlsForComponent(componentName).map { it.controlId }
val changed = Favorites.updateControls(componentName, controls)
if (changed) {
persistenceWrapper.storeFavorites(favoritesAsListLocked())
persistenceWrapper.storeFavorites(Favorites.getAllStructures())
}
val removed = findRemovedLocked(favoritesForComponentKeys.toSet(),
controls)
val removed = findRemoved(favoritesForComponentKeys.toSet(), controls)
val controlsWithFavorite = controls.map {
ControlStatus(it, it.controlId in favoritesForComponentKeys)
}
createLoadDataObject(
currentFavorites.getValue(componentName)
.filter { it.controlId in removed }
.map { createRemovedStatus(it) } +
controlsWithFavorite,
favoritesForComponentKeys
val loadData = createLoadDataObject(
Favorites.getControlsForComponent(componentName)
.filter { it.controlId in removed }
.map { createRemovedStatus(componentName, it) } +
controlsWithFavorite,
favoritesForComponentKeys
)
dataCallback.accept(loadData)
}
dataCallback.accept(loadData)
}
override fun error(message: String) {
val loadData = synchronized(currentFavorites) {
val favoritesForComponent = currentFavorites.getValue(componentName)
val favoritesForComponentKeys = favoritesForComponent
.map { it.controlId }
createLoadDataObject(
favoritesForComponent.map { createRemovedStatus(it, false) },
favoritesForComponentKeys,
val loadData = Favorites.getControlsForComponent(componentName).let {
controls ->
val keys = controls.map { it.controlId }
createLoadDataObject(
controls.map { createRemovedStatus(componentName, it, false) },
keys,
true
)
)
}
dataCallback.accept(loadData)
}
}
@@ -284,15 +265,16 @@ class ControlsControllerImpl @Inject constructor (
}
private fun createRemovedStatus(
componentName: ComponentName,
controlInfo: ControlInfo,
setRemoved: Boolean = true
): ControlStatus {
val intent = Intent(Intent.ACTION_MAIN).apply {
addCategory(Intent.CATEGORY_LAUNCHER)
this.`package` = controlInfo.component.packageName
this.`package` = componentName.packageName
}
val pendingIntent = PendingIntent.getActivity(context,
controlInfo.component.hashCode(),
componentName.hashCode(),
intent,
0)
val control = Control.StatelessBuilder(controlInfo.controlId, pendingIntent)
@@ -302,50 +284,15 @@ class ControlsControllerImpl @Inject constructor (
return ControlStatus(control, true, setRemoved)
}
@GuardedBy("currentFavorites")
private fun findRemovedLocked(favoriteKeys: Set<String>, list: List<Control>): Set<String> {
private fun findRemoved(favoriteKeys: Set<String>, list: List<Control>): Set<String> {
val controlsKeys = list.map { it.controlId }
return favoriteKeys.minus(controlsKeys)
}
@GuardedBy("currentFavorites")
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 { 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
}
}
}
return changed
}
@GuardedBy("currentFavorites")
private fun favoritesAsListLocked(): List<ControlInfo> {
return currentFavorites.flatMap { it.value }
}
override fun subscribeToFavorites() {
override fun subscribeToFavorites(structureInfo: StructureInfo) {
if (!confirmAvailability()) return
// Make a copy of the favorites list
val favorites = synchronized(currentFavorites) {
currentFavorites.flatMap { it.value }
}
bindingController.subscribe(favorites)
bindingController.subscribe(structureInfo)
}
override fun unsubscribe() {
@@ -353,44 +300,12 @@ class ControlsControllerImpl @Inject constructor (
bindingController.unsubscribe()
}
override fun changeFavoriteStatus(controlInfo: ControlInfo, state: Boolean) {
override fun replaceFavoritesForStructure(structureInfo: StructureInfo) {
if (!confirmAvailability()) return
var changed = false
val listOfControls = synchronized(currentFavorites) {
if (state) {
if (controlInfo.component !in currentFavorites) {
currentFavorites.put(controlInfo.component, mutableListOf())
changed = true
}
val controlsForComponent = currentFavorites.getValue(controlInfo.component)
if (controlsForComponent.firstOrNull {
it.controlId == controlInfo.controlId
} == null) {
controlsForComponent.add(controlInfo)
changed = true
}
} else {
changed = currentFavorites.get(controlInfo.component)
?.remove(controlInfo) != null
}
favoritesAsListLocked()
executor.execute {
Favorites.replaceControls(structureInfo)
persistenceWrapper.storeFavorites(Favorites.getAllStructures())
}
if (changed) {
persistenceWrapper.storeFavorites(listOfControls)
}
}
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) {
@@ -399,17 +314,12 @@ class ControlsControllerImpl @Inject constructor (
return
}
executor.execute {
synchronized(currentFavorites) {
val favoriteKeysForComponent =
currentFavorites.get(componentName)?.map { it.controlId } ?: emptyList()
val changed = updateFavoritesLocked(
componentName,
listOf(control),
favoriteKeysForComponent
)
if (changed) {
persistenceWrapper.storeFavorites(favoritesAsListLocked())
}
val changed = Favorites.updateControls(
componentName,
listOf(control)
)
if (changed) {
persistenceWrapper.storeFavorites(Favorites.getAllStructures())
}
}
uiController.onRefreshState(componentName, listOf(control))
@@ -420,41 +330,22 @@ class ControlsControllerImpl @Inject constructor (
uiController.onActionResponse(componentName, controlId, response)
}
override fun getFavoriteControls(): List<ControlInfo> {
if (!confirmAvailability()) return emptyList()
synchronized(currentFavorites) {
return favoritesAsListLocked()
}
}
override fun action(controlInfo: ControlInfo, action: ControlAction) {
override fun action(
componentName: ComponentName,
controlInfo: ControlInfo,
action: ControlAction
) {
if (!confirmAvailability()) return
bindingController.action(controlInfo, action)
bindingController.action(componentName, controlInfo, action)
}
override fun clearFavorites() {
if (!confirmAvailability()) return
val changed = synchronized(currentFavorites) {
currentFavorites.isNotEmpty().also {
currentFavorites.clear()
}
}
if (changed) {
persistenceWrapper.storeFavorites(emptyList())
}
}
override fun getFavorites(): List<StructureInfo> = Favorites.getAllStructures()
override fun countFavoritesForComponent(componentName: ComponentName): Int {
return synchronized(currentFavorites) {
currentFavorites.get(componentName)?.size ?: 0
}
}
override fun countFavoritesForComponent(componentName: ComponentName): Int =
Favorites.getControlsForComponent(componentName).size
override fun getFavoritesForComponent(componentName: ComponentName): List<ControlInfo> {
return synchronized(currentFavorites) {
currentFavorites.get(componentName) ?: emptyList()
}
}
override fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo> =
Favorites.getStructuresForComponent(componentName)
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
pw.println("ControlsController state:")
@@ -462,11 +353,114 @@ class ControlsControllerImpl @Inject constructor (
pw.println(" Changing users: $userChanging")
pw.println(" Current user: ${currentUser.identifier}")
pw.println(" Favorites:")
synchronized(currentFavorites) {
favoritesAsListLocked().forEach {
pw.println(" ${ it }")
Favorites.getAllStructures().forEach { s ->
pw.println(" ${ s }")
s.controls.forEach { c ->
pw.println(" ${ c }")
}
}
pw.println(bindingController.toString())
}
}
}
/**
* Relies on immutable data for thread safety. When necessary to update favMap, use reassignment to
* replace it, which will not disrupt any ongoing map traversal.
*
* Update/replace calls should use thread isolation to avoid race conditions.
*/
private object Favorites {
private var favMap = mapOf<ComponentName, List<StructureInfo>>()
fun getAllStructures(): List<StructureInfo> = favMap.flatMap { it.value }
fun getStructuresForComponent(componentName: ComponentName): List<StructureInfo> =
favMap.get(componentName) ?: emptyList()
fun getControlsForStructure(structure: StructureInfo): List<ControlInfo> =
getStructuresForComponent(structure.componentName)
.firstOrNull { it.structure == structure.structure }
?.controls ?: emptyList()
fun getControlsForComponent(componentName: ComponentName): List<ControlInfo> =
getStructuresForComponent(componentName).flatMap { it.controls }
fun load(structures: List<StructureInfo>) {
favMap = structures.groupBy { it.componentName }
}
fun updateControls(componentName: ComponentName, controls: List<Control>): Boolean {
val controlsById = controls.associateBy { it.controlId }
// utilize a new map to allow for changes to structure names
val structureToControls = mutableMapOf<CharSequence, MutableList<ControlInfo>>()
// Must retain the current control order within each structure
var changed = false
getStructuresForComponent(componentName).forEach { s ->
s.controls.forEach { c ->
val (sName, ci) = controlsById.get(c.controlId)?.let { updatedControl ->
val controlInfo = if (updatedControl.title != c.controlTitle ||
updatedControl.deviceType != c.deviceType) {
changed = true
c.copy(
controlTitle = updatedControl.title,
deviceType = updatedControl.deviceType
)
} else { c }
val updatedStructure = updatedControl.structure ?: ""
if (s.structure != updatedStructure) {
changed = true
}
Pair(updatedStructure, controlInfo)
} ?: Pair(s.structure, c)
structureToControls.getOrPut(sName, { mutableListOf() }).add(ci)
}
}
if (!changed) return false
val structures = structureToControls.map { (s, cs) -> StructureInfo(componentName, s, cs) }
val newFavMap = favMap.toMutableMap()
newFavMap.put(componentName, structures)
favMap = newFavMap
return true
}
fun removeStructures(componentName: ComponentName) {
val newFavMap = favMap.toMutableMap()
newFavMap.remove(componentName)
favMap = newFavMap
}
fun replaceControls(updatedStructure: StructureInfo) {
val newFavMap = favMap.toMutableMap()
val structures = mutableListOf<StructureInfo>()
val componentName = updatedStructure.componentName
var replaced = false
getStructuresForComponent(componentName).forEach { s ->
val newStructure = if (s.structure == updatedStructure.structure) {
replaced = true
updatedStructure
} else { s }
structures.add(newStructure)
}
if (!replaced) {
structures.add(updatedStructure)
}
newFavMap.put(componentName, structures.toList())
favMap = newFavMap.toMap()
}
fun clear() {
favMap = mapOf<ComponentName, List<StructureInfo>>()
}
}

View File

@@ -45,11 +45,17 @@ class ControlsFavoritePersistenceWrapper(
private const val TAG = "ControlsFavoritePersistenceWrapper"
const val FILE_NAME = "controls_favorites.xml"
private const val TAG_CONTROLS = "controls"
private const val TAG_STRUCTURES = "structures"
private const val TAG_STRUCTURE = "structure"
private const val TAG_CONTROL = "control"
private const val TAG_COMPONENT = "component"
private const val TAG_ID = "id"
private const val TAG_TITLE = "title"
private const val TAG_TYPE = "type"
private const val TAG_VERSION = "version"
// must increment with every change to the XML structure
private const val VERSION = 1
}
/**
@@ -66,7 +72,7 @@ class ControlsFavoritePersistenceWrapper(
*
* @param list a list of favorite controls. The list will be stored in the same order.
*/
fun storeFavorites(list: List<ControlInfo>) {
fun storeFavorites(structures: List<StructureInfo>) {
executor.execute {
Log.d(TAG, "Saving data to file: $file")
val atomicFile = AtomicFile(file)
@@ -81,16 +87,28 @@ class ControlsFavoritePersistenceWrapper(
setOutput(writer, "utf-8")
setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
startDocument(null, true)
startTag(null, TAG_CONTROLS)
list.forEach {
startTag(null, TAG_CONTROL)
attribute(null, TAG_COMPONENT, it.component.flattenToString())
attribute(null, TAG_ID, it.controlId)
attribute(null, TAG_TITLE, it.controlTitle.toString())
attribute(null, TAG_TYPE, it.deviceType.toString())
endTag(null, TAG_CONTROL)
startTag(null, TAG_VERSION)
text("$VERSION")
endTag(null, TAG_VERSION)
startTag(null, TAG_STRUCTURES)
structures.forEach { s ->
startTag(null, TAG_STRUCTURE)
attribute(null, TAG_COMPONENT, s.componentName.flattenToString())
attribute(null, TAG_STRUCTURE, s.structure.toString())
startTag(null, TAG_CONTROLS)
s.controls.forEach { c ->
startTag(null, TAG_CONTROL)
attribute(null, TAG_ID, c.controlId)
attribute(null, TAG_TITLE, c.controlTitle.toString())
attribute(null, TAG_TYPE, c.deviceType.toString())
endTag(null, TAG_CONTROL)
}
endTag(null, TAG_CONTROLS)
endTag(null, TAG_STRUCTURE)
}
endTag(null, TAG_CONTROLS)
endTag(null, TAG_STRUCTURES)
endDocument()
atomicFile.finishWrite(writer)
}
@@ -109,7 +127,7 @@ class ControlsFavoritePersistenceWrapper(
* @return a list of stored favorite controls. Return an empty list if the file is not found
* @throws [IllegalStateException] if there is an error while reading the file
*/
fun readFavorites(): List<ControlInfo> {
fun readFavorites(): List<StructureInfo> {
if (!file.exists()) {
Log.d(TAG, "No favorites, returning empty list")
return emptyList()
@@ -134,25 +152,32 @@ class ControlsFavoritePersistenceWrapper(
}
}
private fun parseXml(parser: XmlPullParser): List<ControlInfo> {
private fun parseXml(parser: XmlPullParser): List<StructureInfo> {
var type: Int
val infos = mutableListOf<ControlInfo>()
val infos = mutableListOf<StructureInfo>()
var lastComponent: ComponentName? = null
var lastStructure: CharSequence? = null
var controls = mutableListOf<ControlInfo>()
while (parser.next().also { type = it } != XmlPullParser.END_DOCUMENT) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue
}
val tagName = parser.name
if (tagName == TAG_CONTROL) {
val component = ComponentName.unflattenFromString(
parser.getAttributeValue(null, TAG_COMPONENT))
val tagName = parser.name ?: ""
if (type == XmlPullParser.START_TAG && tagName == TAG_STRUCTURE) {
lastComponent = ComponentName.unflattenFromString(
parser.getAttributeValue(null, TAG_COMPONENT))
lastStructure = parser.getAttributeValue(null, TAG_STRUCTURE) ?: ""
} else if (type == XmlPullParser.START_TAG && tagName == TAG_CONTROL) {
val id = parser.getAttributeValue(null, TAG_ID)
val title = parser.getAttributeValue(null, TAG_TITLE)
val deviceType = parser.getAttributeValue(null, TAG_TYPE)?.toInt()
if (component != null && id != null && title != null && deviceType != null) {
infos.add(ControlInfo(component, id, title, deviceType))
if (id != null && title != null && deviceType != null) {
controls.add(ControlInfo(id, title, deviceType))
}
} else if (type == XmlPullParser.END_TAG && tagName == TAG_STRUCTURE) {
infos.add(StructureInfo(lastComponent!!, lastStructure!!, controls.toList()))
controls.clear()
}
}
return infos
}
}
}

View File

@@ -96,29 +96,30 @@ class ControlsProviderLifecycleManager(
}
private fun bindService(bind: Boolean) {
requiresBound = bind
if (bind) {
if (bindTryCount == MAX_BIND_RETRIES) {
return
executor.execute {
requiresBound = bind
if (bind) {
if (bindTryCount != MAX_BIND_RETRIES) {
if (DEBUG) {
Log.d(TAG, "Binding service $intent")
}
bindTryCount++
try {
context.bindServiceAsUser(intent, serviceConnection, BIND_FLAGS, user)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to bind to service", e)
}
}
} else {
if (DEBUG) {
Log.d(TAG, "Unbinding service $intent")
}
bindTryCount = 0
wrapper?.run {
context.unbindService(serviceConnection)
}
wrapper = null
}
if (DEBUG) {
Log.d(TAG, "Binding service $intent")
}
bindTryCount++
try {
context.bindServiceAsUser(intent, serviceConnection, BIND_FLAGS, user)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to bind to service", e)
}
} else {
if (DEBUG) {
Log.d(TAG, "Unbinding service $intent")
}
bindTryCount = 0
wrapper?.run {
context.unbindService(serviceConnection)
}
wrapper = null
}
}
@@ -320,6 +321,9 @@ class ControlsProviderLifecycleManager(
onLoadCanceller?.run()
onLoadCanceller = null
// just in case this wasn't called already
unsubscribe()
bindService(false)
}

View File

@@ -0,0 +1,34 @@
/*
* 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.controller
import android.content.ComponentName
/**
* Stores basic information about a Structure to persist and keep track of favorites.
*
* Every [component] [structure] pair uniquely identifies the structure.
*
* @property componentName the name of the component that provides the [Control].
* @property structure common structure name of all underlying [controls], or empty string
* @property controls all controls in the name structure
*/
data class StructureInfo(
val componentName: ComponentName,
val structure: CharSequence,
val controls: List<ControlInfo>
)

View File

@@ -26,15 +26,13 @@ import android.widget.TextView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.android.settingslib.applications.DefaultAppInfo
import com.android.settingslib.widget.CandidateInfo
import com.android.systemui.R
import com.android.systemui.controls.ControlsServiceInfo
import java.text.Collator
import java.util.concurrent.Executor
/**
* Adapter for binding [CandidateInfo] related to [ControlsProviderService].
* Adapter for binding [ControlsServiceInfo] related to [ControlsProviderService].
*
* This class handles subscribing and keeping track of the list of valid applications for
* displaying.
@@ -56,16 +54,16 @@ class AppAdapter(
private val resources: Resources
) : RecyclerView.Adapter<AppAdapter.Holder>() {
private var listOfServices = emptyList<CandidateInfo>()
private var listOfServices = emptyList<ControlsServiceInfo>()
private val callback = object : ControlsListingController.ControlsListingCallback {
override fun onServicesUpdated(candidates: List<ControlsServiceInfo>) {
override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
backgroundExecutor.execute {
val collator = Collator.getInstance(resources.configuration.locales[0])
val localeComparator = compareBy<CandidateInfo, CharSequence>(collator) {
val localeComparator = compareBy<ControlsServiceInfo, CharSequence>(collator) {
it.loadLabel()
}
listOfServices = candidates.sortedWith(localeComparator)
listOfServices = serviceInfos.sortedWith(localeComparator)
uiExecutor.execute(::notifyDataSetChanged)
}
}
@@ -101,11 +99,10 @@ class AppAdapter(
* Bind data to the view
* @param data Information about the [ControlsProviderService] to bind to the data
*/
fun bindData(data: CandidateInfo) {
fun bindData(data: ControlsServiceInfo) {
icon.setImageDrawable(data.loadIcon())
title.text = data.loadLabel()
favorites.text = favRenderer.renderFavoritesForComponent(
(data as DefaultAppInfo).componentName)
favorites.text = favRenderer.renderFavoritesForComponent(data.componentName)
}
}
}
@@ -123,4 +120,4 @@ class FavoritesRenderer(
return ""
}
}
}
}

View File

@@ -29,6 +29,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.controls.controller.StructureInfo
import com.android.systemui.controls.controller.ControlsControllerImpl
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.settings.CurrentUserTracker
@@ -52,6 +53,7 @@ class ControlsFavoritingActivity @Inject constructor(
private lateinit var statusText: TextView
private var model: ControlsModel? = null
private var component: ComponentName? = null
private var structureName: CharSequence = ""
private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
private val startingUser = controller.currentUserId
@@ -97,11 +99,11 @@ class ControlsFavoritingActivity @Inject constructor(
requireViewById<Button>(R.id.done).setOnClickListener {
if (component == null) return@setOnClickListener
val favoritesForStorage = model?.favorites?.map {
it.componentName = component!!
it.build()
}
if (favoritesForStorage != null) {
controller.replaceFavoritesForComponent(component!!, favoritesForStorage)
controller.replaceFavoritesForStructure(StructureInfo(component!!, structureName,
favoritesForStorage))
finishAffinity()
}
}
@@ -112,6 +114,12 @@ class ControlsFavoritingActivity @Inject constructor(
val allControls = data.allControls
val favoriteKeys = data.favoritesIds
val error = data.errorOnLoad
val structures = allControls.fold(hashSetOf<CharSequence>()) {
s, c ->
s.add(c.control.structure ?: "")
s
}
// TODO add multi structure switching support
executor.execute {
val emptyZoneString = resources.getText(
R.string.controls_favorite_other_zone_header)
@@ -149,4 +157,4 @@ class ControlsFavoritingActivity @Inject constructor(
currentUserTracker.stopTracking()
super.onDestroy()
}
}
}

View File

@@ -45,6 +45,6 @@ interface ControlsListingController :
@FunctionalInterface
interface ControlsListingCallback {
fun onServicesUpdated(candidates: List<ControlsServiceInfo>)
fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>)
}
}

View File

@@ -24,7 +24,6 @@ import android.os.UserHandle
import android.service.controls.ControlsProviderService
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
import com.android.settingslib.applications.DefaultAppInfo
import com.android.settingslib.applications.ServiceListing
import com.android.settingslib.widget.CandidateInfo
import com.android.systemui.controls.ControlsServiceInfo
@@ -157,7 +156,7 @@ class ControlsListingControllerImpl @VisibleForTesting constructor(
* @return a label as returned by [CandidateInfo.loadLabel] or `null`.
*/
override fun getAppLabel(name: ComponentName): CharSequence? {
return getCurrentServices().firstOrNull { (it as? DefaultAppInfo)?.componentName == name }
return getCurrentServices().firstOrNull { it.componentName == name }
?.loadLabel()
}
}
}

View File

@@ -122,7 +122,7 @@ class ControlViewHolder(
}
fun action(action: ControlAction) {
controlsController.action(cws.ci, action)
controlsController.action(cws.componentName, cws.ci, action)
}
private fun findBehavior(status: Int, template: ControlTemplate): KClass<out Behavior> {

View File

@@ -16,6 +16,7 @@
package com.android.systemui.controls.ui
import android.content.ComponentName
import android.service.controls.Control
import com.android.systemui.controls.controller.ControlInfo
@@ -23,9 +24,14 @@ import com.android.systemui.controls.controller.ControlInfo
/**
* A container for:
* <ul>
* <li>ComponentName - Component responsible for this Control
* <li>ControlInfo - Basic cached info about a Control
* <li>Control - Actual Control parcelable received directly from
* the participating application
* </ul>
*/
data class ControlWithState(val ci: ControlInfo, val control: Control?)
data class ControlWithState(
val componentName: ComponentName,
val ci: ControlInfo,
val control: Control?
)

View File

@@ -22,27 +22,33 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.SharedPreferences
import android.graphics.drawable.Drawable
import android.os.IBinder
import android.service.controls.Control
import android.service.controls.TokenProvider
import android.util.Log
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ListPopupWindow
import android.widget.Space
import com.android.settingslib.widget.CandidateInfo
import com.android.systemui.controls.controller.ControlsController
import android.widget.TextView
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.controller.ControlInfo
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.controller.StructureInfo
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.management.ControlsProviderSelectorActivity
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.R
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.util.concurrency.DelayableExecutor
import dagger.Lazy
@@ -56,8 +62,11 @@ import javax.inject.Singleton
private const val TOKEN = "https://www.googleapis.com/auth/assistant"
private const val SCOPE = "oauth2:" + TOKEN
private var tokenProviderConnection: TokenProviderConnection? = null
class TokenProviderConnection(val cc: ControlsController, val context: Context)
: ServiceConnection {
class TokenProviderConnection(
val cc: ControlsController,
val context: Context,
val structure: StructureInfo?
) : ServiceConnection {
private var mTokenProvider: TokenProvider? = null
override fun onServiceConnected(cName: ComponentName, binder: IBinder) {
@@ -71,7 +80,9 @@ class TokenProviderConnection(val cc: ControlsController, val context: Context)
Log.e(ControlsUiController.TAG, "NO ACCOUNT IS SET. Open HomeMock app")
} else {
mTokenProvider?.setAuthToken(getAuthToken(mLastAccountName))
cc.subscribeToFavorites()
structure?.let {
cc.subscribeToFavorites(it)
}
}
}, "TokenProviderThread").start()
}
@@ -117,83 +128,115 @@ class ControlsUiControllerImpl @Inject constructor (
val context: Context,
@Main val uiExecutor: DelayableExecutor,
@Background val bgExecutor: DelayableExecutor,
val controlsListingController: Lazy<ControlsListingController>
val controlsListingController: Lazy<ControlsListingController>,
@Main val sharedPreferences: SharedPreferences
) : ControlsUiController {
private lateinit var controlInfos: List<ControlInfo>
companion object {
private const val PREF_COMPONENT = "controls_component"
private const val PREF_STRUCTURE = "controls_structure"
private val EMPTY_COMPONENT = ComponentName("", "")
private val EMPTY_STRUCTURE = StructureInfo(
EMPTY_COMPONENT,
"",
mutableListOf<ControlInfo>()
)
}
private var selectedStructure: StructureInfo = EMPTY_STRUCTURE
private lateinit var allStructures: List<StructureInfo>
private val controlsById = mutableMapOf<ControlKey, ControlWithState>()
private val controlViewsById = mutableMapOf<ControlKey, ControlViewHolder>()
private lateinit var parent: ViewGroup
private lateinit var lastItems: List<SelectionItem>
private var popup: ListPopupWindow? = null
private val addControlsItem = SelectionItem(
context.resources.getString(R.string.controls_providers_title),
"",
context.getDrawable(R.drawable.ic_add),
EMPTY_COMPONENT
)
override val available: Boolean
get() = controlsController.get().available
private val listingCallback = object : ControlsListingController.ControlsListingCallback {
override fun onServicesUpdated(candidates: List<ControlsServiceInfo>) {
bgExecutor.execute {
val collator = Collator.getInstance(context.resources.configuration.locales[0])
val localeComparator = compareBy<CandidateInfo, CharSequence>(collator) {
it.loadLabel()
}
private lateinit var listingCallback: ControlsListingController.ControlsListingCallback
val mList = candidates.toMutableList()
mList.sortWith(localeComparator)
loadInitialSetupViewIcons(mList.map { it.loadLabel() to it.loadIcon() })
private fun createCallback(
onResult: (List<SelectionItem>) -> Unit
): ControlsListingController.ControlsListingCallback {
return object : ControlsListingController.ControlsListingCallback {
override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
bgExecutor.execute {
val collator = Collator.getInstance(context.resources.configuration.locales[0])
val localeComparator = compareBy<ControlsServiceInfo, CharSequence>(collator) {
it.loadLabel()
}
val mList = serviceInfos.toMutableList()
mList.sortWith(localeComparator)
lastItems = mList.map {
SelectionItem(it.loadLabel(), "", it.loadIcon(), it.componentName)
}
uiExecutor.execute {
onResult(lastItems)
}
}
}
}
}
override fun show(parent: ViewGroup) {
Log.d(ControlsUiController.TAG, "show()")
this.parent = parent
controlInfos = controlsController.get().getFavoriteControls()
allStructures = controlsController.get().getFavorites()
selectedStructure = loadPreference(allStructures)
controlInfos.map {
ControlWithState(it, null)
}.associateByTo(controlsById) { ControlKey(it.ci.component, it.ci.controlId) }
if (controlInfos.isEmpty()) {
showInitialSetupView()
if (selectedStructure.controls.isEmpty() && allStructures.size <= 1) {
// only show initial view if there are really no favorites across any structure
listingCallback = createCallback(::showInitialSetupView)
} else {
showControlsView()
selectedStructure.controls.map {
ControlWithState(selectedStructure.componentName, it, null)
}.associateByTo(controlsById) {
ControlKey(selectedStructure.componentName, it.ci.controlId)
}
listingCallback = createCallback(::showControlsView)
}
controlsListingController.get().addCallback(listingCallback)
// Temp code to pass auth
tokenProviderConnection = TokenProviderConnection(controlsController.get(), context)
tokenProviderConnection = TokenProviderConnection(controlsController.get(), context,
selectedStructure)
val serviceIntent = Intent()
serviceIntent.setComponent(ComponentName("com.android.systemui.home.mock",
"com.android.systemui.home.mock.AuthService"))
if (!context.bindService(serviceIntent, tokenProviderConnection!!,
Context.BIND_AUTO_CREATE)) {
controlsController.get().subscribeToFavorites()
controlsController.get().subscribeToFavorites(selectedStructure)
}
}
private fun showInitialSetupView() {
private fun showInitialSetupView(items: List<SelectionItem>) {
parent.removeAllViews()
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.controls_no_favorites, parent, true)
val viewGroup = parent.requireViewById(R.id.controls_no_favorites_group) as ViewGroup
viewGroup.setOnClickListener(launchSelectorActivityListener(context))
controlsListingController.get().addCallback(listingCallback)
}
private fun loadInitialSetupViewIcons(icons: List<Pair<CharSequence, Drawable>>) {
uiExecutor.execute {
val viewGroup = parent.requireViewById(R.id.controls_icon_row) as ViewGroup
viewGroup.removeAllViews()
val inflater = LayoutInflater.from(context)
icons.forEach {
val imageView = inflater.inflate(R.layout.controls_icon, viewGroup, false)
as ImageView
imageView.setContentDescription(it.first)
imageView.setImageDrawable(it.second)
viewGroup.addView(imageView)
}
val iconRowGroup = parent.requireViewById(R.id.controls_icon_row) as ViewGroup
items.forEach {
val imageView = inflater.inflate(R.layout.controls_icon, viewGroup, false) as ImageView
imageView.setContentDescription(it.getTitle())
imageView.setImageDrawable(it.icon)
iconRowGroup.addView(imageView)
}
}
@@ -209,14 +252,16 @@ class ControlsUiControllerImpl @Inject constructor (
}
}
private fun showControlsView() {
private fun showControlsView(items: List<SelectionItem>) {
parent.removeAllViews()
controlViewsById.clear()
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.controls_with_favorites, parent, true)
val listView = parent.requireViewById(R.id.global_actions_controls_list) as ViewGroup
var lastRow: ViewGroup = createRow(inflater, listView)
controlInfos.forEach {
Log.d(ControlsUiController.TAG, "favorited control id: " + it.controlId)
selectedStructure.controls.forEach {
if (lastRow.getChildCount() == 2) {
lastRow = createRow(inflater, listView)
}
@@ -224,21 +269,114 @@ class ControlsUiControllerImpl @Inject constructor (
R.layout.controls_base_item, lastRow, false) as ViewGroup
lastRow.addView(item)
val cvh = ControlViewHolder(item, controlsController.get(), uiExecutor, bgExecutor)
val key = ControlKey(it.component, it.controlId)
val key = ControlKey(selectedStructure.componentName, it.controlId)
cvh.bindData(controlsById.getValue(key))
controlViewsById.put(key, cvh)
}
if ((controlInfos.size % 2) == 1) {
// add spacer if necessary to keep control size consistent
if ((selectedStructure.controls.size % 2) == 1) {
lastRow.addView(Space(context), LinearLayout.LayoutParams(0, 0, 1f))
}
val moreImageView = parent.requireViewById(R.id.controls_more) as View
moreImageView.setOnClickListener(launchSelectorActivityListener(context))
val itemsByComponent = items.associateBy { it.componentName }
var adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply {
val listItems = allStructures.mapNotNull {
itemsByComponent.get(it.componentName)?.copy(structure = it.structure)
}
addAll(listItems + addControlsItem)
}
/*
* Default spinner widget does not work with the window type required
* for this dialog. Use a textView with the ListPopupWindow to achieve
* a similar effect
*/
parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply {
setText((adapter.findSelectionItem(selectedStructure) ?: adapter.getItem(0)).getTitle())
}
val anchor = parent.requireViewById<ViewGroup>(R.id.controls_header)
anchor.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
popup = ListPopupWindow(
ContextThemeWrapper(context, R.style.Control_ListPopupWindow))
popup?.apply {
setWindowLayoutType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
setAnchorView(anchor)
setAdapter(adapter)
setModal(true)
setOnItemClickListener(object : AdapterView.OnItemClickListener {
override fun onItemClick(
parent: AdapterView<*>,
view: View,
pos: Int,
id: Long
) {
val listItem = parent.getItemAtPosition(pos) as SelectionItem
this@ControlsUiControllerImpl.switchAppOrStructure(listItem)
dismiss()
}
})
// need to call show() first in order to construct the listView
show()
getListView()?.apply {
setDividerHeight(
context.resources.getDimensionPixelSize(R.dimen.control_list_divider))
setDivider(
context.resources.getDrawable(R.drawable.controls_list_divider))
}
show()
}
}
})
parent.requireViewById<ImageView>(R.id.app_icon).apply {
setContentDescription("My Home")
setImageDrawable(items[0].icon)
}
}
private fun loadPreference(structures: List<StructureInfo>): StructureInfo {
if (structures.isEmpty()) return EMPTY_STRUCTURE
val component = sharedPreferences.getString(PREF_COMPONENT, null)?.let {
ComponentName.unflattenFromString(it)
} ?: EMPTY_COMPONENT
val structure = sharedPreferences.getString(PREF_STRUCTURE, "")
return structures.firstOrNull {
component == it.componentName && structure == it.structure
} ?: structures.get(0)
}
private fun updatePreferences(si: StructureInfo) {
sharedPreferences.edit()
.putString(PREF_COMPONENT, si.componentName.flattenToString())
.putString(PREF_STRUCTURE, si.structure.toString())
.commit()
}
private fun switchAppOrStructure(item: SelectionItem) {
if (item == addControlsItem) {
launchSelectorActivityListener(context)(parent)
} else {
val newSelection = allStructures.first {
it.structure == item.structure && it.componentName == item.componentName
}
if (newSelection != selectedStructure) {
selectedStructure = newSelection
updatePreferences(selectedStructure)
showControlsView(lastItems)
}
}
}
override fun hide() {
Log.d(ControlsUiController.TAG, "hide()")
popup?.dismiss()
controlsController.get().unsubscribe()
context.unbindService(tokenProviderConnection)
tokenProviderConnection = null
@@ -254,7 +392,7 @@ class ControlsUiControllerImpl @Inject constructor (
controls.forEach { c ->
controlsById.get(ControlKey(componentName, c.getControlId()))?.let {
Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId())
val cws = ControlWithState(it.ci, c)
val cws = ControlWithState(componentName, it.ci, c)
val key = ControlKey(componentName, c.getControlId())
controlsById.put(key, cws)
@@ -278,3 +416,46 @@ class ControlsUiControllerImpl @Inject constructor (
return row
}
}
private data class SelectionItem(
val appName: CharSequence,
val structure: CharSequence,
val icon: Drawable,
val componentName: ComponentName
) {
fun getTitle() = if (structure.isEmpty()) { appName } else { structure }
}
private class ItemAdapter(
val parentContext: Context,
val resource: Int
) : ArrayAdapter<SelectionItem>(parentContext, resource) {
val layoutInflater = LayoutInflater.from(context)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val item = getItem(position)
val view = convertView ?: layoutInflater.inflate(resource, parent, false)
view.requireViewById<TextView>(R.id.controls_spinner_item).apply {
setText(item.getTitle())
}
view.requireViewById<ImageView>(R.id.app_icon).apply {
setContentDescription(item.getTitle())
setImageDrawable(item.icon)
}
return view
}
fun findSelectionItem(si: StructureInfo): SelectionItem? {
var i = 0
while (i < getCount()) {
val item = getItem(i)
if (item.componentName == si.componentName &&
item.structure == si.structure) {
return item
}
i++
}
return null
}
}

View File

@@ -39,7 +39,6 @@ import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -68,6 +67,7 @@ class ControlsBindingControllerImplTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
providers.clear()
controller = TestableControlsBindingControllerImpl(
mContext, executor, Lazy { mockControlsController })
@@ -77,7 +77,6 @@ class ControlsBindingControllerImplTest : SysuiTestCase() {
fun tearDown() {
executor.advanceClockToLast()
executor.runAllReady()
providers.clear()
}
@Test
@@ -94,71 +93,56 @@ class ControlsBindingControllerImplTest : SysuiTestCase() {
}
controller.bindAndLoad(TEST_COMPONENT_NAME_1, callback)
assertEquals(1, providers.size)
val provider = providers.first()
verify(provider).maybeBindAndLoad(any())
verify(providers[0]).maybeBindAndLoad(any())
}
@Test
fun testBindServices() {
controller.bindServices(listOf(TEST_COMPONENT_NAME_1, TEST_COMPONENT_NAME_2))
fun testBindService() {
controller.bindService(TEST_COMPONENT_NAME_1)
executor.runAllReady()
assertEquals(2, providers.size)
assertEquals(setOf(TEST_COMPONENT_NAME_1, TEST_COMPONENT_NAME_2),
providers.map { it.componentName }.toSet())
providers.forEach {
verify(it).bindService()
}
verify(providers[0]).bindService()
}
@Test
fun testSubscribe() {
val controlInfo1 = ControlInfo(TEST_COMPONENT_NAME_1, "id_1", "", DeviceTypes.TYPE_UNKNOWN)
val controlInfo2 = ControlInfo(TEST_COMPONENT_NAME_2, "id_2", "", DeviceTypes.TYPE_UNKNOWN)
controller.bindServices(listOf(TEST_COMPONENT_NAME_3))
val controlInfo1 = ControlInfo("id_1", "", DeviceTypes.TYPE_UNKNOWN)
val controlInfo2 = ControlInfo("id_2", "", DeviceTypes.TYPE_UNKNOWN)
val structure =
StructureInfo(TEST_COMPONENT_NAME_1, "Home", listOf(controlInfo1, controlInfo2))
controller.subscribe(listOf(controlInfo1, controlInfo2))
controller.subscribe(structure)
executor.runAllReady()
assertEquals(3, providers.size)
val provider1 = providers.first { it.componentName == TEST_COMPONENT_NAME_1 }
val provider2 = providers.first { it.componentName == TEST_COMPONENT_NAME_2 }
val provider3 = providers.first { it.componentName == TEST_COMPONENT_NAME_3 }
verify(provider1).maybeBindAndSubscribe(listOf(controlInfo1.controlId))
verify(provider2).maybeBindAndSubscribe(listOf(controlInfo2.controlId))
verify(provider3, never()).maybeBindAndSubscribe(any())
verify(provider3).unbindService() // Not needed services will be unbound
verify(providers[0]).maybeBindAndSubscribe(
listOf(controlInfo1.controlId, controlInfo2.controlId))
}
@Test
fun testUnsubscribe_notRefreshing() {
controller.bindServices(listOf(TEST_COMPONENT_NAME_1, TEST_COMPONENT_NAME_2))
controller.bindService(TEST_COMPONENT_NAME_2)
controller.unsubscribe()
executor.runAllReady()
providers.forEach {
verify(it, never()).unsubscribe()
}
verify(providers[0], never()).unsubscribe()
}
@Test
fun testUnsubscribe_refreshing() {
val controlInfo1 = ControlInfo(TEST_COMPONENT_NAME_1, "id_1", "", DeviceTypes.TYPE_UNKNOWN)
val controlInfo2 = ControlInfo(TEST_COMPONENT_NAME_2, "id_2", "", DeviceTypes.TYPE_UNKNOWN)
val controlInfo1 = ControlInfo("id_1", "", DeviceTypes.TYPE_UNKNOWN)
val controlInfo2 = ControlInfo("id_2", "", DeviceTypes.TYPE_UNKNOWN)
val structure =
StructureInfo(TEST_COMPONENT_NAME_1, "Home", listOf(controlInfo1, controlInfo2))
controller.subscribe(listOf(controlInfo1, controlInfo2))
controller.subscribe(structure)
controller.unsubscribe()
executor.runAllReady()
providers.forEach {
verify(it).unsubscribe()
}
verify(providers[0]).unsubscribe()
}
@Test
@@ -169,49 +153,37 @@ class ControlsBindingControllerImplTest : SysuiTestCase() {
@Test
fun testChangeUsers_providersHaveCorrectUser() {
controller.bindServices(listOf(TEST_COMPONENT_NAME_1))
controller.changeUser(otherUser)
controller.bindServices(listOf(TEST_COMPONENT_NAME_2))
controller.bindService(TEST_COMPONENT_NAME_1)
assertEquals(user, providers[0].user)
val provider1 = providers.first { it.componentName == TEST_COMPONENT_NAME_1 }
assertEquals(user, provider1.user)
val provider2 = providers.first { it.componentName == TEST_COMPONENT_NAME_2 }
assertEquals(otherUser, provider2.user)
controller.changeUser(otherUser)
controller.bindService(TEST_COMPONENT_NAME_2)
assertEquals(otherUser, providers[0].user)
}
@Test
fun testChangeUsers_providersUnbound() {
controller.bindServices(listOf(TEST_COMPONENT_NAME_1))
controller.bindService(TEST_COMPONENT_NAME_1)
controller.changeUser(otherUser)
val provider1 = providers.first { it.componentName == TEST_COMPONENT_NAME_1 }
verify(provider1).unbindService()
verify(providers[0]).unbindService()
controller.bindServices(listOf(TEST_COMPONENT_NAME_2))
controller.bindService(TEST_COMPONENT_NAME_2)
controller.changeUser(user)
reset(provider1)
val provider2 = providers.first { it.componentName == TEST_COMPONENT_NAME_2 }
verify(provider2).unbindService()
verify(provider1, never()).unbindService()
verify(providers[0]).unbindService()
}
@Test
fun testComponentRemoved_existingIsUnbound() {
controller.bindServices(listOf(
TEST_COMPONENT_NAME_1,
TEST_COMPONENT_NAME_2,
TEST_COMPONENT_NAME_3
))
controller.bindService(TEST_COMPONENT_NAME_1)
controller.onComponentRemoved(TEST_COMPONENT_NAME_2)
controller.onComponentRemoved(TEST_COMPONENT_NAME_1)
executor.runAllReady()
providers.forEach {
verify(it, if (it.componentName == TEST_COMPONENT_NAME_2) times(1) else never())
.unbindService()
}
verify(providers[0], times(1)).unbindService()
}
}
@@ -222,7 +194,7 @@ class TestableControlsBindingControllerImpl(
) : ControlsBindingControllerImpl(context, executor, lazyController) {
companion object {
val providers = mutableSetOf<ControlsProviderLifecycleManager>()
val providers = mutableListOf<ControlsProviderLifecycleManager>()
}
// Replaces the real provider with a mock and puts the mock in a visible set.
@@ -235,7 +207,10 @@ class TestableControlsBindingControllerImpl(
`when`(provider.componentName).thenReturn(realProvider.componentName)
`when`(provider.token).thenReturn(token)
`when`(provider.user).thenReturn(realProvider.user)
providers.clear()
providers.add(provider)
return provider
}
}

View File

@@ -79,7 +79,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
private lateinit var listingController: ControlsListingController
@Captor
private lateinit var controlInfoListCaptor: ArgumentCaptor<List<ControlInfo>>
private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo>
@Captor
private lateinit var controlLoadCallbackCaptor:
ArgumentCaptor<ControlsBindingController.LoadCallback>
@@ -101,15 +101,21 @@ class ControlsControllerImplTest : SysuiTestCase() {
private const val TEST_CONTROL_ID = "control1"
private const val TEST_CONTROL_TITLE = "Test"
private const val TEST_DEVICE_TYPE = DeviceTypes.TYPE_AC_HEATER
private val TEST_CONTROL_INFO = ControlInfo(
TEST_COMPONENT, TEST_CONTROL_ID, TEST_CONTROL_TITLE, TEST_DEVICE_TYPE)
private const val TEST_STRUCTURE = ""
private val TEST_CONTROL_INFO = ControlInfo(TEST_CONTROL_ID,
TEST_CONTROL_TITLE, TEST_DEVICE_TYPE)
private val TEST_STRUCTURE_INFO = StructureInfo(TEST_COMPONENT,
TEST_STRUCTURE, listOf(TEST_CONTROL_INFO))
private val TEST_COMPONENT_2 = ComponentName("test.pkg", "test.class.2")
private const val TEST_CONTROL_ID_2 = "control2"
private const val TEST_CONTROL_TITLE_2 = "Test 2"
private const val TEST_DEVICE_TYPE_2 = DeviceTypes.TYPE_CAMERA
private val TEST_CONTROL_INFO_2 = ControlInfo(
TEST_COMPONENT_2, TEST_CONTROL_ID_2, TEST_CONTROL_TITLE_2, TEST_DEVICE_TYPE_2)
private const val TEST_STRUCTURE_2 = "My House"
private val TEST_CONTROL_INFO_2 = ControlInfo(TEST_CONTROL_ID_2,
TEST_CONTROL_TITLE_2, TEST_DEVICE_TYPE_2)
private val TEST_STRUCTURE_INFO_2 = StructureInfo(TEST_COMPONENT_2,
TEST_STRUCTURE_2, listOf(TEST_CONTROL_INFO_2))
}
private val user = mContext.userId
@@ -145,6 +151,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
assertTrue(controller.available)
verify(broadcastDispatcher).registerReceiver(
capture(broadcastReceiverCaptor), any(), any(), eq(UserHandle.ALL))
verify(listingController).addCallback(capture(listingCallbackCaptor))
}
@@ -160,12 +167,12 @@ class ControlsControllerImplTest : SysuiTestCase() {
@Test
fun testStartWithoutFavorites() {
assertTrue(controller.getFavoriteControls().isEmpty())
assertTrue(controller.getFavorites().isEmpty())
}
@Test
fun testStartWithSavedFavorites() {
`when`(persistenceWrapper.readFavorites()).thenReturn(listOf(TEST_CONTROL_INFO))
`when`(persistenceWrapper.readFavorites()).thenReturn(listOf(TEST_STRUCTURE_INFO))
val controller_other = ControlsControllerImpl(
mContext,
delayableExecutor,
@@ -176,88 +183,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
Optional.of(persistenceWrapper),
mock(DumpManager::class.java)
)
assertEquals(listOf(TEST_CONTROL_INFO), controller_other.getFavoriteControls())
}
@Test
fun testAddFavorite() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
val favorites = controller.getFavoriteControls()
assertTrue(TEST_CONTROL_INFO in favorites)
assertEquals(1, favorites.size)
}
@Test
fun testAddMultipleFavorites() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.changeFavoriteStatus(TEST_CONTROL_INFO_2, true)
val favorites = controller.getFavoriteControls()
assertTrue(TEST_CONTROL_INFO in favorites)
assertTrue(TEST_CONTROL_INFO_2 in favorites)
assertEquals(2, favorites.size)
}
@Test
fun testAddAndRemoveFavorite() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.changeFavoriteStatus(TEST_CONTROL_INFO_2, true)
controller.changeFavoriteStatus(TEST_CONTROL_INFO, false)
val favorites = controller.getFavoriteControls()
assertTrue(TEST_CONTROL_INFO !in favorites)
assertTrue(TEST_CONTROL_INFO_2 in favorites)
assertEquals(1, favorites.size)
}
@Test
fun testFavoritesSavedOnAdd() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
verify(persistenceWrapper).storeFavorites(listOf(TEST_CONTROL_INFO))
}
@Test
fun testFavoritesSavedOnRemove() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
reset(persistenceWrapper)
controller.changeFavoriteStatus(TEST_CONTROL_INFO, false)
verify(persistenceWrapper).storeFavorites(emptyList())
}
@Test
fun testFavoritesSavedOnChange() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2)
val control = builderFromInfo(newControlInfo).build()
controller.loadForComponent(TEST_COMPONENT, Consumer {})
reset(persistenceWrapper)
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
capture(controlLoadCallbackCaptor))
controlLoadCallbackCaptor.value.accept(listOf(control))
verify(persistenceWrapper).storeFavorites(listOf(newControlInfo))
}
@Test
fun testFavoritesNotSavedOnRedundantAdd() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
reset(persistenceWrapper)
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
verify(persistenceWrapper, never()).storeFavorites(ArgumentMatchers.anyList())
}
@Test
fun testFavoritesNotSavedOnNotRemove() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, false)
verify(persistenceWrapper, never()).storeFavorites(ArgumentMatchers.anyList())
assertEquals(listOf(TEST_STRUCTURE_INFO), controller_other.getFavorites())
}
@Test
@@ -285,15 +211,16 @@ class ControlsControllerImplTest : SysuiTestCase() {
@Test
fun testSubscribeFavorites() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.changeFavoriteStatus(TEST_CONTROL_INFO_2, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO_2)
delayableExecutor.runAllReady()
controller.subscribeToFavorites()
controller.subscribeToFavorites(TEST_STRUCTURE_INFO)
verify(bindingController).subscribe(capture(controlInfoListCaptor))
verify(bindingController).subscribe(capture(structureInfoCaptor))
assertTrue(TEST_CONTROL_INFO in controlInfoListCaptor.value)
assertTrue(TEST_CONTROL_INFO_2 in controlInfoListCaptor.value)
assertTrue(TEST_CONTROL_INFO in structureInfoCaptor.value.controls)
assertFalse(TEST_CONTROL_INFO_2 in structureInfoCaptor.value.controls)
}
@Test
@@ -318,6 +245,8 @@ class ControlsControllerImplTest : SysuiTestCase() {
controlLoadCallbackCaptor.value.accept(listOf(control))
delayableExecutor.runAllReady()
assertTrue(loaded)
}
@@ -326,7 +255,9 @@ class ControlsControllerImplTest : SysuiTestCase() {
var loaded = false
val control = builderFromInfo(TEST_CONTROL_INFO).build()
val control2 = builderFromInfo(TEST_CONTROL_INFO_2).build()
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO_2)
delayableExecutor.runAllReady()
controller.loadForComponent(TEST_COMPONENT, Consumer { data ->
val controls = data.allControls
@@ -348,6 +279,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
capture(controlLoadCallbackCaptor))
controlLoadCallbackCaptor.value.accept(listOf(control, control2))
delayableExecutor.runAllReady()
assertTrue(loaded)
}
@@ -355,7 +287,8 @@ class ControlsControllerImplTest : SysuiTestCase() {
@Test
fun testLoadForComponent_removed() {
var loaded = false
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
delayableExecutor.runAllReady()
controller.loadForComponent(TEST_COMPONENT, Consumer { data ->
val controls = data.allControls
@@ -376,6 +309,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
capture(controlLoadCallbackCaptor))
controlLoadCallbackCaptor.value.accept(emptyList())
delayableExecutor.runAllReady()
assertTrue(loaded)
}
@@ -383,7 +317,8 @@ class ControlsControllerImplTest : SysuiTestCase() {
@Test
fun testErrorOnLoad_notRemoved() {
var loaded = false
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
delayableExecutor.runAllReady()
controller.loadForComponent(TEST_COMPONENT, Consumer { data ->
val controls = data.allControls
@@ -410,7 +345,9 @@ class ControlsControllerImplTest : SysuiTestCase() {
@Test
fun testFavoriteInformationModifiedOnLoad() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
delayableExecutor.runAllReady()
val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2)
val control = builderFromInfo(newControlInfo).build()
@@ -420,15 +357,17 @@ class ControlsControllerImplTest : SysuiTestCase() {
capture(controlLoadCallbackCaptor))
controlLoadCallbackCaptor.value.accept(listOf(control))
delayableExecutor.runAllReady()
val favorites = controller.getFavoriteControls()
val favorites = controller.getFavorites().flatMap { it.controls }
assertEquals(1, favorites.size)
assertEquals(newControlInfo, favorites[0])
}
@Test
fun testFavoriteInformationModifiedOnRefresh() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2)
val control = builderFromInfo(newControlInfo).build()
@@ -436,23 +375,15 @@ class ControlsControllerImplTest : SysuiTestCase() {
delayableExecutor.runAllReady()
val favorites = controller.getFavoriteControls()
val favorites = controller.getFavorites().flatMap { it.controls }
assertEquals(1, favorites.size)
assertEquals(newControlInfo, favorites[0])
}
@Test
fun testClearFavorites() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
assertEquals(1, controller.getFavoriteControls().size)
controller.clearFavorites()
assertTrue(controller.getFavoriteControls().isEmpty())
}
@Test
fun testSwitchUsers() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
delayableExecutor.runAllReady()
reset(persistenceWrapper)
val intent = Intent(Intent.ACTION_USER_SWITCHED).apply {
@@ -468,7 +399,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
verify(persistenceWrapper).readFavorites()
verify(bindingController).changeUser(UserHandle.of(otherUser))
verify(listingController).changeUser(UserHandle.of(otherUser))
assertTrue(controller.getFavoriteControls().isEmpty())
assertTrue(controller.getFavorites().isEmpty())
assertEquals(otherUser, controller.currentUserId)
assertTrue(controller.available)
}
@@ -483,24 +414,28 @@ class ControlsControllerImplTest : SysuiTestCase() {
@Test
fun testDisableFeature_clearFavorites() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
assertFalse(controller.getFavoriteControls().isEmpty())
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
delayableExecutor.runAllReady()
assertFalse(controller.getFavorites().isEmpty())
Settings.Secure.putIntForUser(mContext.contentResolver,
ControlsControllerImpl.CONTROLS_AVAILABLE, 0, user)
controller.settingObserver.onChange(false, ControlsControllerImpl.URI, user)
assertTrue(controller.getFavoriteControls().isEmpty())
assertTrue(controller.getFavorites().isEmpty())
}
@Test
fun testDisableFeature_noChangeForNotCurrentUser() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
delayableExecutor.runAllReady()
Settings.Secure.putIntForUser(mContext.contentResolver,
ControlsControllerImpl.CONTROLS_AVAILABLE, 0, otherUser)
controller.settingObserver.onChange(false, ControlsControllerImpl.URI, otherUser)
assertTrue(controller.available)
assertFalse(controller.getFavoriteControls().isEmpty())
assertFalse(controller.getFavorites().isEmpty())
}
@Test
@@ -522,7 +457,8 @@ class ControlsControllerImplTest : SysuiTestCase() {
@Test
fun testCountFavoritesForComponent_singleComponent() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
delayableExecutor.runAllReady()
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(0, controller.countFavoritesForComponent(TEST_COMPONENT_2))
@@ -530,8 +466,9 @@ class ControlsControllerImplTest : SysuiTestCase() {
@Test
fun testCountFavoritesForComponent_multipleComponents() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.changeFavoriteStatus(TEST_CONTROL_INFO_2, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO_2)
delayableExecutor.runAllReady()
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT_2))
@@ -539,84 +476,123 @@ class ControlsControllerImplTest : SysuiTestCase() {
@Test
fun testGetFavoritesForComponent() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
assertEquals(listOf(TEST_CONTROL_INFO), controller.getFavoritesForComponent(TEST_COMPONENT))
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
delayableExecutor.runAllReady()
assertEquals(listOf(TEST_STRUCTURE_INFO),
controller.getFavoritesForComponent(TEST_COMPONENT))
}
@Test
fun testGetFavoritesForComponent_otherComponent() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO_2, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO_2)
delayableExecutor.runAllReady()
assertTrue(controller.getFavoritesForComponent(TEST_COMPONENT).isEmpty())
}
@Test
fun testGetFavoritesForComponent_multipleInOrder() {
val controlInfo = ControlInfo(TEST_COMPONENT, "id", "title", 0)
val controlInfo = ControlInfo("id", "title", 0)
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.changeFavoriteStatus(controlInfo, true)
controller.replaceFavoritesForStructure(
StructureInfo(
TEST_COMPONENT,
"Home",
listOf(TEST_CONTROL_INFO, controlInfo)
))
delayableExecutor.runAllReady()
assertEquals(listOf(TEST_CONTROL_INFO, controlInfo),
controller.getFavoritesForComponent(TEST_COMPONENT))
controller.getFavoritesForComponent(TEST_COMPONENT).flatMap { it.controls })
controller.clearFavorites()
controller.changeFavoriteStatus(controlInfo, true)
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.replaceFavoritesForStructure(
StructureInfo(
TEST_COMPONENT,
"Home",
listOf(controlInfo, TEST_CONTROL_INFO)
))
delayableExecutor.runAllReady()
assertEquals(listOf(controlInfo, TEST_CONTROL_INFO),
controller.getFavoritesForComponent(TEST_COMPONENT).flatMap { it.controls })
}
@Test
fun testReplaceFavoritesForStructure_noFavorites() {
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
delayableExecutor.runAllReady()
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOf(TEST_STRUCTURE_INFO),
controller.getFavoritesForComponent(TEST_COMPONENT))
}
@Test
fun testReplaceFavoritesForComponent_noFavorites() {
controller.replaceFavoritesForComponent(TEST_COMPONENT, listOf(TEST_CONTROL_INFO))
fun testReplaceFavoritesForStructure_differentComponentsAreFilteredOut() {
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO_2)
delayableExecutor.runAllReady()
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOf(TEST_CONTROL_INFO), controller.getFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOf(TEST_CONTROL_INFO),
controller.getFavoritesForComponent(TEST_COMPONENT).flatMap { it.controls })
}
@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)
fun testReplaceFavoritesForStructure_oldFavoritesRemoved() {
val controlInfo = ControlInfo("id", "title", 0)
assertNotEquals(TEST_CONTROL_INFO, controlInfo)
controller.changeFavoriteStatus(controlInfo, true)
controller.replaceFavoritesForComponent(TEST_COMPONENT, listOf(TEST_CONTROL_INFO))
val newComponent = ComponentName("test.pkg", "test.class.3")
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOf(TEST_CONTROL_INFO), controller.getFavoritesForComponent(TEST_COMPONENT))
controller.replaceFavoritesForStructure(
StructureInfo(
newComponent,
"Home",
listOf(controlInfo)
))
controller.replaceFavoritesForStructure(
StructureInfo(
newComponent,
"Home",
listOf(TEST_CONTROL_INFO)
))
delayableExecutor.runAllReady()
assertEquals(1, controller.countFavoritesForComponent(newComponent))
assertEquals(listOf(TEST_CONTROL_INFO), controller
.getFavoritesForComponent(newComponent).flatMap { it.controls })
}
@Test
fun testReplaceFavoritesForComponent_favoritesInOrder() {
val controlInfo = ControlInfo(TEST_COMPONENT, "id", "title", 0)
fun testReplaceFavoritesForStructure_favoritesInOrder() {
val controlInfo = ControlInfo("id", "title", 0)
val listOrder1 = listOf(TEST_CONTROL_INFO, controlInfo)
controller.replaceFavoritesForComponent(TEST_COMPONENT, listOrder1)
val structure1 = StructureInfo(TEST_COMPONENT, "Home", listOrder1)
controller.replaceFavoritesForStructure(structure1)
delayableExecutor.runAllReady()
assertEquals(2, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOrder1, controller.getFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOrder1, controller.getFavoritesForComponent(TEST_COMPONENT)
.flatMap { it.controls })
val listOrder2 = listOf(controlInfo, TEST_CONTROL_INFO)
controller.replaceFavoritesForComponent(TEST_COMPONENT, listOrder2)
val structure2 = StructureInfo(TEST_COMPONENT, "Home", listOrder2)
controller.replaceFavoritesForStructure(structure2)
delayableExecutor.runAllReady()
assertEquals(2, controller.countFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOrder2, controller.getFavoritesForComponent(TEST_COMPONENT))
assertEquals(listOrder2, controller.getFavoritesForComponent(TEST_COMPONENT)
.flatMap { it.controls })
}
@Test
fun testPackageRemoved_noFavorites_noRemovals() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
delayableExecutor.runAllReady()
val serviceInfo = mock(ServiceInfo::class.java)
`when`(serviceInfo.componentName).thenReturn(TEST_COMPONENT)
@@ -625,21 +601,21 @@ class ControlsControllerImplTest : SysuiTestCase() {
// Don't want to check what happens before this call
reset(persistenceWrapper)
listingCallbackCaptor.value.onServicesUpdated(listOf(info))
delayableExecutor.runAllReady()
verify(bindingController, never()).onComponentRemoved(any())
assertEquals(1, controller.getFavoriteControls().size)
assertEquals(TEST_CONTROL_INFO, controller.getFavoriteControls()[0])
assertEquals(1, controller.getFavorites().size)
assertEquals(TEST_STRUCTURE_INFO, controller.getFavorites()[0])
verify(persistenceWrapper, never()).storeFavorites(ArgumentMatchers.anyList())
}
@Test
fun testPackageRemoved_hasFavorites() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
controller.changeFavoriteStatus(TEST_CONTROL_INFO_2, true)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO_2)
delayableExecutor.runAllReady()
val serviceInfo = mock(ServiceInfo::class.java)
`when`(serviceInfo.componentName).thenReturn(TEST_COMPONENT)
@@ -647,14 +623,14 @@ class ControlsControllerImplTest : SysuiTestCase() {
// Don't want to check what happens before this call
reset(persistenceWrapper)
listingCallbackCaptor.value.onServicesUpdated(listOf(info))
listingCallbackCaptor.value.onServicesUpdated(listOf(info))
delayableExecutor.runAllReady()
verify(bindingController).onComponentRemoved(TEST_COMPONENT_2)
assertEquals(1, controller.getFavoriteControls().size)
assertEquals(TEST_CONTROL_INFO, controller.getFavoriteControls()[0])
assertEquals(1, controller.getFavorites().size)
assertEquals(TEST_STRUCTURE_INFO, controller.getFavorites()[0])
verify(persistenceWrapper).storeFavorites(ArgumentMatchers.anyList())
}

View File

@@ -55,18 +55,27 @@ class ControlsFavoritePersistenceWrapperTest : SysuiTestCase() {
@Test
fun testSaveAndRestore() {
val controlInfo1 = ControlInfo(
ComponentName.unflattenFromString("TEST_PKG/.TEST_CLS_1")!!,
"id1", "name_1", DeviceTypes.TYPE_UNKNOWN)
val controlInfo2 = ControlInfo(
ComponentName.unflattenFromString("TEST_PKG/.TEST_CLS_2")!!,
"id2", "name_2", DeviceTypes.TYPE_GENERIC_ON_OFF)
val list = listOf(controlInfo1, controlInfo2)
val structureInfo1 = StructureInfo(
ComponentName.unflattenFromString("TEST_PKG/.TEST_CLS_1")!!,
"",
listOf(
ControlInfo("id1", "name_1", DeviceTypes.TYPE_UNKNOWN)
)
)
val structureInfo2 = StructureInfo(
ComponentName.unflattenFromString("TEST_PKG/.TEST_CLS_2")!!,
"structure1",
listOf(
ControlInfo("id2", "name_2", DeviceTypes.TYPE_GENERIC_ON_OFF),
ControlInfo("id3", "name_3", DeviceTypes.TYPE_GENERIC_ON_OFF)
)
)
val list = listOf(structureInfo1, structureInfo2)
wrapper.storeFavorites(list)
executor.runAllReady()
assertEquals(list, wrapper.readFavorites())
}
}
}