Merge changes I77671bd4,I22021266 into rvc-dev
* changes: Controls UI - Structure switching Controls - Structure persistence
This commit is contained in:
23
packages/SystemUI/res/drawable/controls_list_divider.xml
Normal file
23
packages/SystemUI/res/drawable/controls_list_divider.xml
Normal 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>
|
||||
@@ -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>
|
||||
50
packages/SystemUI/res/layout/controls_spinner_item.xml
Normal file
50
packages/SystemUI/res/layout/controls_spinner_item.xml
Normal 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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].
|
||||
*/
|
||||
|
||||
@@ -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>>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,6 @@ interface ControlsListingController :
|
||||
|
||||
@FunctionalInterface
|
||||
interface ControlsListingCallback {
|
||||
fun onServicesUpdated(candidates: List<ControlsServiceInfo>)
|
||||
fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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?
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user