Add multi-user support to Controls

This commit adds multi-user support by doing the following:
* Listening when user changes and switching the controllers user (and
context).
* Using activities that show for all users and are finished on user
switched.

The setting has to be enabled for each user separately.

Also:
* fixes calling subscribe when on load to the ControlsProviderService.
* better dumps.

Test: atest
Test: check that files are not shared between users
Test: check that user folder is removed when user is deleted
Bug:147732882
Change-Id: I349b0136473016e6bd6b71e26045f11a839272d1
This commit is contained in:
Fabian Kozynski
2020-01-30 12:21:52 -05:00
parent c5f436ec5e
commit 7988bd464d
19 changed files with 477 additions and 98 deletions

View File

@@ -646,6 +646,7 @@
android:label="Controls Providers"
android:theme="@style/Theme.SystemUI"
android:exported="true"
android:showForAllUsers="true"
android:excludeFromRecents="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
android:visibleToInstantApps="true">
@@ -655,6 +656,7 @@
android:parentActivityName=".controls.management.ControlsProviderSelectorActivity"
android:theme="@style/Theme.SystemUI"
android:excludeFromRecents="true"
android:showForAllUsers="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
android:visibleToInstantApps="true">
</activity>

View File

@@ -18,4 +18,8 @@ package com.android.systemui.controls
import android.service.controls.Control
data class ControlStatus(val control: Control, val favorite: Boolean, val removed: Boolean = false)
data class ControlStatus(
val control: Control,
val favorite: Boolean,
val removed: Boolean = false
)

View File

@@ -22,7 +22,7 @@ import com.android.settingslib.applications.DefaultAppInfo
class ControlsServiceInfo(
context: Context,
serviceInfo: ServiceInfo
val serviceInfo: ServiceInfo
) : DefaultAppInfo(
context,
context.packageManager,

View File

@@ -0,0 +1,25 @@
/*
* 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
import android.os.UserHandle
interface UserAwareController {
fun changeUser(newUser: UserHandle) {}
val currentUserId: Int
}

View File

@@ -19,8 +19,9 @@ package com.android.systemui.controls.controller
import android.content.ComponentName
import android.service.controls.Control
import android.service.controls.actions.ControlAction
import com.android.systemui.controls.UserAwareController
interface ControlsBindingController {
interface ControlsBindingController : UserAwareController {
fun bindAndLoad(component: ComponentName, callback: (List<Control>) -> Unit)
fun bindServices(components: List<ComponentName>)
fun subscribe(controls: List<ControlInfo>)

View File

@@ -19,6 +19,7 @@ package com.android.systemui.controls.controller
import android.content.ComponentName
import android.content.Context
import android.os.IBinder
import android.os.UserHandle
import android.service.controls.Control
import android.service.controls.IControlsActionCallback
import android.service.controls.IControlsLoadCallback
@@ -50,12 +51,17 @@ open class ControlsBindingControllerImpl @Inject constructor(
private val refreshing = AtomicBoolean(false)
private var currentUser = context.user
override val currentUserId: Int
get() = currentUser.identifier
@GuardedBy("componentMap")
private val tokenMap: MutableMap<IBinder, ControlsProviderLifecycleManager> =
ArrayMap<IBinder, ControlsProviderLifecycleManager>()
@GuardedBy("componentMap")
private val componentMap: MutableMap<ComponentName, ControlsProviderLifecycleManager> =
ArrayMap<ComponentName, ControlsProviderLifecycleManager>()
private val componentMap: MutableMap<Key, ControlsProviderLifecycleManager> =
ArrayMap<Key, ControlsProviderLifecycleManager>()
private val loadCallbackService = object : IControlsLoadCallback.Stub() {
override fun accept(token: IBinder, controls: MutableList<Control>) {
@@ -103,6 +109,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
loadCallbackService,
actionCallbackService,
subscriberService,
currentUser,
component
)
}
@@ -110,7 +117,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
private fun retrieveLifecycleManager(component: ComponentName):
ControlsProviderLifecycleManager {
synchronized(componentMap) {
val provider = componentMap.getOrPut(component) {
val provider = componentMap.getOrPut(Key(component, currentUser)) {
createProviderManager(component)
}
tokenMap.putIfAbsent(provider.token, provider)
@@ -137,7 +144,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
val providersWithFavorites = controlsByComponentName.keys
synchronized(componentMap) {
componentMap.forEach {
if (it.key !in providersWithFavorites) {
if (it.key.component !in providersWithFavorites) {
backgroundExecutor.execute { it.value.unbindService() }
}
}
@@ -167,6 +174,36 @@ open class ControlsBindingControllerImpl @Inject constructor(
}
}
override fun changeUser(newUser: UserHandle) {
if (newUser == currentUser) return
synchronized(componentMap) {
unbindAllProvidersLocked() // unbind all providers from the old user
}
refreshing.set(false)
currentUser = newUser
}
private fun unbindAllProvidersLocked() {
componentMap.values.forEach {
if (it.user == currentUser) {
it.unbindService()
}
}
}
override fun toString(): String {
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")
}
}
}.toString()
}
private abstract inner class CallbackRunnable(val token: IBinder) : Runnable {
protected val provider: ControlsProviderLifecycleManager? =
synchronized(componentMap) {
@@ -183,6 +220,10 @@ open class ControlsBindingControllerImpl @Inject constructor(
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")
@@ -204,6 +245,10 @@ open class ControlsBindingControllerImpl @Inject constructor(
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)
}
@@ -229,7 +274,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
) : CallbackRunnable(token) {
override fun run() {
provider?.let {
Log.i(TAG, "onComplete receive from '${provider?.componentName}'")
Log.i(TAG, "onComplete receive from '${provider.componentName}'")
}
}
}
@@ -240,7 +285,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
) : CallbackRunnable(token) {
override fun run() {
provider?.let {
Log.e(TAG, "onError receive from '${provider?.componentName}': $error")
Log.e(TAG, "onError receive from '${provider.componentName}': $error")
}
}
}
@@ -251,9 +296,15 @@ open class ControlsBindingControllerImpl @Inject constructor(
@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
}
provider?.let {
lazyController.get().onActionResponse(it.componentName, controlId, response)
}
}
}
}
private data class Key(val component: ComponentName, val user: UserHandle)

View File

@@ -20,8 +20,9 @@ import android.content.ComponentName
import android.service.controls.Control
import android.service.controls.actions.ControlAction
import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.UserAwareController
interface ControlsController {
interface ControlsController : UserAwareController {
val available: Boolean
fun getFavoriteControls(): List<ControlInfo>

View File

@@ -17,10 +17,13 @@
package com.android.systemui.controls.controller
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Environment
import android.os.UserHandle
import android.provider.Settings
import android.service.controls.Control
import android.service.controls.actions.ControlAction
@@ -29,14 +32,17 @@ import android.util.Log
import com.android.internal.annotations.GuardedBy
import com.android.systemui.DumpController
import com.android.systemui.Dumpable
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.management.ControlsFavoritingActivity
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.util.concurrency.DelayableExecutor
import java.io.FileDescriptor
import java.io.PrintWriter
import java.util.Optional
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@@ -46,35 +52,101 @@ class ControlsControllerImpl @Inject constructor (
@Background private val executor: DelayableExecutor,
private val uiController: ControlsUiController,
private val bindingController: ControlsBindingController,
private val optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>,
private val listingController: ControlsListingController,
broadcastDispatcher: BroadcastDispatcher,
optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>,
dumpController: DumpController
) : Dumpable, ControlsController {
companion object {
private const val TAG = "ControlsControllerImpl"
const val CONTROLS_AVAILABLE = "systemui.controls_available"
const val USER_CHANGE_RETRY_DELAY = 500L // ms
}
override val available = Settings.Secure.getInt(
// Map of map: ComponentName -> (String -> ControlInfo).
// Only for current user
@GuardedBy("currentFavorites")
private val currentFavorites = ArrayMap<ComponentName, MutableMap<String, ControlInfo>>()
private var userChanging = true
override var available = Settings.Secure.getInt(
context.contentResolver, CONTROLS_AVAILABLE, 0) != 0
val persistenceWrapper = optionalWrapper.orElseGet {
private set
private var currentUser = context.user
override val currentUserId
get() = currentUser.identifier
private val persistenceWrapper = optionalWrapper.orElseGet {
ControlsFavoritePersistenceWrapper(
Environment.buildPath(
context.filesDir,
ControlsFavoritePersistenceWrapper.FILE_NAME),
context.filesDir,
ControlsFavoritePersistenceWrapper.FILE_NAME
),
executor
)
}
// Map of map: ComponentName -> (String -> ControlInfo)
@GuardedBy("currentFavorites")
private val currentFavorites = ArrayMap<ComponentName, MutableMap<String, ControlInfo>>()
init {
private fun setValuesForUser(newUser: UserHandle) {
Log.d(TAG, "Changing to user: $newUser")
currentUser = newUser
val userContext = context.createContextAsUser(currentUser, 0)
val fileName = Environment.buildPath(
userContext.filesDir, ControlsFavoritePersistenceWrapper.FILE_NAME)
persistenceWrapper.changeFile(fileName)
available = Settings.Secure.getIntForUser(
context.contentResolver, CONTROLS_AVAILABLE, 0) != 0
synchronized(currentFavorites) {
currentFavorites.clear()
}
if (available) {
dumpController.registerDumpable(this)
loadFavorites()
}
bindingController.changeUser(newUser)
listingController.changeUser(newUser)
userChanging = false
}
private val userSwitchReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_USER_SWITCHED) {
userChanging = true
val newUser =
UserHandle.of(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, sendingUserId))
if (currentUser == newUser) {
userChanging = false
return
}
setValuesForUser(newUser)
}
}
}
init {
dumpController.registerDumpable(this)
if (available) {
loadFavorites()
}
userChanging = false
broadcastDispatcher.registerReceiver(
userSwitchReceiver,
IntentFilter(Intent.ACTION_USER_SWITCHED),
executor,
UserHandle.ALL
)
}
private fun confirmAvailability(): Boolean {
if (userChanging) {
Log.w(TAG, "Controls not available while user is changing")
return false
}
if (!available) {
Log.d(TAG, "Controls not available")
return false
}
return true
}
private fun loadFavorites() {
@@ -91,8 +163,18 @@ class ControlsControllerImpl @Inject constructor (
componentName: ComponentName,
callback: (List<ControlStatus>) -> Unit
) {
if (!available) {
Log.d(TAG, "Controls not available")
if (!confirmAvailability()) {
if (userChanging) {
// Try again later, userChanging should not last forever. If so, we have bigger
// problems
executor.executeDelayed(
{ loadForComponent(componentName, callback) },
USER_CHANGE_RETRY_DELAY,
TimeUnit.MILLISECONDS
)
} else {
callback(emptyList())
}
return
}
bindingController.bindAndLoad(componentName) {
@@ -158,10 +240,7 @@ class ControlsControllerImpl @Inject constructor (
}
override fun subscribeToFavorites() {
if (!available) {
Log.d(TAG, "Controls not available")
return
}
if (!confirmAvailability()) return
// Make a copy of the favorites list
val favorites = synchronized(currentFavorites) {
currentFavorites.flatMap { it.value.values.toList() }
@@ -170,18 +249,12 @@ class ControlsControllerImpl @Inject constructor (
}
override fun unsubscribe() {
if (!available) {
Log.d(TAG, "Controls not available")
return
}
if (!confirmAvailability()) return
bindingController.unsubscribe()
}
override fun changeFavoriteStatus(controlInfo: ControlInfo, state: Boolean) {
if (!available) {
Log.d(TAG, "Controls not available")
return
}
if (!confirmAvailability()) return
var changed = false
val listOfControls = synchronized(currentFavorites) {
if (state) {
@@ -211,7 +284,7 @@ class ControlsControllerImpl @Inject constructor (
}
override fun refreshStatus(componentName: ComponentName, control: Control) {
if (!available) {
if (!confirmAvailability()) {
Log.d(TAG, "Controls not available")
return
}
@@ -227,28 +300,24 @@ class ControlsControllerImpl @Inject constructor (
}
override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
if (!available) {
Log.d(TAG, "Controls not available")
return
}
if (!confirmAvailability()) return
uiController.onActionResponse(componentName, controlId, response)
}
override fun getFavoriteControls(): List<ControlInfo> {
if (!available) {
Log.d(TAG, "Controls not available")
return emptyList()
}
if (!confirmAvailability()) return emptyList()
synchronized(currentFavorites) {
return favoritesAsListLocked()
}
}
override fun action(controlInfo: ControlInfo, action: ControlAction) {
if (!confirmAvailability()) return
bindingController.action(controlInfo, action)
}
override fun clearFavorites() {
if (!confirmAvailability()) return
val changed = synchronized(currentFavorites) {
currentFavorites.isNotEmpty().also {
currentFavorites.clear()
@@ -261,6 +330,9 @@ class ControlsControllerImpl @Inject constructor (
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
pw.println("ControlsController state:")
pw.println(" Available: $available")
pw.println(" Changing users: $userChanging")
pw.println(" Current user: ${currentUser.identifier}")
pw.println(" Favorites:")
synchronized(currentFavorites) {
currentFavorites.forEach {
@@ -269,5 +341,6 @@ class ControlsControllerImpl @Inject constructor (
}
}
}
pw.println(bindingController.toString())
}
}
}

View File

@@ -16,7 +16,6 @@
package com.android.systemui.controls.controller
import android.app.ActivityManager
import android.content.ComponentName
import android.util.AtomicFile
import android.util.Log
@@ -32,8 +31,8 @@ import java.io.FileNotFoundException
import java.io.IOException
class ControlsFavoritePersistenceWrapper(
val file: File,
val executor: DelayableExecutor
private var file: File,
private var executor: DelayableExecutor
) {
companion object {
@@ -47,11 +46,13 @@ class ControlsFavoritePersistenceWrapper(
private const val TAG_TYPE = "type"
}
val currentUser: Int
get() = ActivityManager.getCurrentUser()
fun changeFile(fileName: File) {
file = fileName
}
fun storeFavorites(list: List<ControlInfo>) {
executor.execute {
Log.d(TAG, "Saving data to file: $file")
val atomicFile = AtomicFile(file)
val writer = try {
atomicFile.startWrite()
@@ -98,6 +99,7 @@ class ControlsFavoritePersistenceWrapper(
return emptyList()
}
try {
Log.d(TAG, "Reading data from file: $file")
val parser = Xml.newPullParser()
parser.setInput(reader, null)
return parseXml(parser)
@@ -111,7 +113,7 @@ class ControlsFavoritePersistenceWrapper(
}
private fun parseXml(parser: XmlPullParser): List<ControlInfo> {
var type: Int = 0
var type = 0
val infos = mutableListOf<ControlInfo>()
while (parser.next().also { type = it } != XmlPullParser.END_DOCUMENT) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
@@ -123,9 +125,9 @@ class ControlsFavoritePersistenceWrapper(
parser.getAttributeValue(null, TAG_COMPONENT))
val id = parser.getAttributeValue(null, TAG_ID)
val title = parser.getAttributeValue(null, TAG_TITLE)
val type = parser.getAttributeValue(null, TAG_TYPE)?.toInt()
if (component != null && id != null && title != null && type != null) {
infos.add(ControlInfo(component, id, title, type))
val deviceType = parser.getAttributeValue(null, TAG_TYPE)?.toInt()
if (component != null && id != null && title != null && deviceType != null) {
infos.add(ControlInfo(component, id, title, deviceType))
}
}
}

View File

@@ -24,6 +24,7 @@ import android.os.Binder
import android.os.Bundle
import android.os.IBinder
import android.os.RemoteException
import android.os.UserHandle
import android.service.controls.Control
import android.service.controls.ControlsProviderService.CALLBACK_BUNDLE
import android.service.controls.ControlsProviderService.CALLBACK_TOKEN
@@ -46,6 +47,7 @@ class ControlsProviderLifecycleManager(
private val loadCallbackService: IControlsLoadCallback.Stub,
private val actionCallbackService: IControlsActionCallback.Stub,
private val subscriberService: IControlsSubscriber.Stub,
val user: UserHandle,
val componentName: ComponentName
) : IBinder.DeathRecipient {
@@ -96,7 +98,7 @@ class ControlsProviderLifecycleManager(
}
bindTryCount++
try {
isBound = context.bindService(intent, serviceConnection, BIND_FLAGS)
isBound = context.bindServiceAsUser(intent, serviceConnection, BIND_FLAGS, user)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to bind to service", e)
isBound = false
@@ -152,7 +154,9 @@ class ControlsProviderLifecycleManager(
load()
}
queue.filter { it is Message.Subscribe }.flatMap { (it as Message.Subscribe).list }.run {
subscribe(this)
if (this.isNotEmpty()) {
subscribe(this)
}
}
queue.filter { it is Message.Action }.forEach {
val msg = it as Message.Action
@@ -286,6 +290,15 @@ class ControlsProviderLifecycleManager(
maybeUnbindAndRemoveCallback()
}
override fun toString(): String {
return StringBuilder("ControlsProviderLifecycleManager(").apply {
append("component=$componentName")
append(", user=$user")
append(", bound=$isBound")
append(")")
}.toString()
}
sealed class Message {
abstract val type: Int
object Load : Message() {
@@ -301,4 +314,4 @@ class ControlsProviderLifecycleManager(
override val type = MSG_ACTION
}
}
}
}

View File

@@ -23,6 +23,7 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.android.settingslib.widget.CandidateInfo
import com.android.systemui.R

View File

@@ -22,15 +22,18 @@ import android.os.Bundle
import android.view.LayoutInflater
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.controls.controller.ControlInfo
import com.android.systemui.controls.controller.ControlsControllerImpl
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.settings.CurrentUserTracker
import java.util.concurrent.Executor
import javax.inject.Inject
class ControlsFavoritingActivity @Inject constructor(
@Main private val executor: Executor,
private val controller: ControlsControllerImpl
private val controller: ControlsControllerImpl,
broadcastDispatcher: BroadcastDispatcher
) : Activity() {
companion object {
@@ -42,11 +45,24 @@ class ControlsFavoritingActivity @Inject constructor(
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: ControlAdapter
private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
private val startingUser = controller.currentUserId
override fun onUserSwitched(newUserId: Int) {
if (newUserId != startingUser) {
stopTracking()
finish()
}
}
}
private var component: ComponentName? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val app = intent.getCharSequenceExtra(EXTRA_APP)
val component = intent.getParcelableExtra<ComponentName>(EXTRA_COMPONENT)
component = intent.getParcelableExtra<ComponentName>(EXTRA_COMPONENT)
// If we have no component name, there's not much we can do.
val callback = component?.let {
@@ -68,6 +84,11 @@ class ControlsFavoritingActivity @Inject constructor(
}
setContentView(recyclerView)
currentUserTracker.startTracking()
}
override fun onResume() {
super.onResume()
component?.let {
controller.loadForComponent(it) {
executor.execute {

View File

@@ -18,14 +18,17 @@ package com.android.systemui.controls.management
import android.content.ComponentName
import com.android.settingslib.widget.CandidateInfo
import com.android.systemui.controls.UserAwareController
import com.android.systemui.statusbar.policy.CallbackController
interface ControlsListingController :
CallbackController<ControlsListingController.ControlsListingCallback> {
CallbackController<ControlsListingController.ControlsListingCallback>,
UserAwareController {
fun getCurrentServices(): List<CandidateInfo>
fun getAppLabel(name: ComponentName): CharSequence? = ""
@FunctionalInterface
interface ControlsListingCallback {
fun onServicesUpdated(list: List<CandidateInfo>)
}

View File

@@ -19,6 +19,7 @@ package com.android.systemui.controls.management
import android.content.ComponentName
import android.content.Context
import android.content.pm.ServiceInfo
import android.os.UserHandle
import android.service.controls.ControlsProviderService
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
@@ -31,6 +32,16 @@ import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Singleton
private fun createServiceListing(context: Context): ServiceListing {
return ServiceListing.Builder(context).apply {
setIntentAction(ControlsProviderService.SERVICE_CONTROLS)
setPermission("android.permission.BIND_CONTROLS")
setNoun("Controls Provider")
setSetting("controls_providers")
setTag("controls_providers")
}.build()
}
/**
* Provides a listing of components to be used as ControlsServiceProvider.
*
@@ -43,41 +54,55 @@ import javax.inject.Singleton
class ControlsListingControllerImpl @VisibleForTesting constructor(
private val context: Context,
@Background private val backgroundExecutor: Executor,
private val serviceListing: ServiceListing
private val serviceListingBuilder: (Context) -> ServiceListing
) : ControlsListingController {
@Inject
constructor(context: Context, executor: Executor): this(
context,
executor,
ServiceListing.Builder(context)
.setIntentAction(ControlsProviderService.SERVICE_CONTROLS)
.setPermission("android.permission.BIND_CONTROLS")
.setNoun("Controls Provider")
.setSetting("controls_providers")
.setTag("controls_providers")
.build()
::createServiceListing
)
private var serviceListing = serviceListingBuilder(context)
companion object {
private const val TAG = "ControlsListingControllerImpl"
}
private var availableServices = emptyList<ServiceInfo>()
init {
serviceListing.addCallback {
Log.d(TAG, "ServiceConfig reloaded")
availableServices = it.toList()
override var currentUserId = context.userId
private set
backgroundExecutor.execute {
callbacks.forEach {
it.onServicesUpdated(getCurrentServices())
}
private val serviceListingCallback = ServiceListing.Callback {
Log.d(TAG, "ServiceConfig reloaded")
availableServices = it.toList()
backgroundExecutor.execute {
callbacks.forEach {
it.onServicesUpdated(getCurrentServices())
}
}
}
init {
serviceListing.addCallback(serviceListingCallback)
}
override fun changeUser(newUser: UserHandle) {
backgroundExecutor.execute {
callbacks.clear()
availableServices = emptyList()
serviceListing.setListening(false)
serviceListing.removeCallback(serviceListingCallback)
currentUserId = newUser.identifier
val contextForUser = context.createContextAsUser(newUser, 0)
serviceListing = serviceListingBuilder(contextForUser)
serviceListing.addCallback(serviceListingCallback)
}
}
// All operations in background thread
private val callbacks = mutableSetOf<ControlsListingController.ControlsListingCallback>()
@@ -91,6 +116,7 @@ class ControlsListingControllerImpl @VisibleForTesting constructor(
*/
override fun addCallback(listener: ControlsListingController.ControlsListingCallback) {
backgroundExecutor.execute {
Log.d(TAG, "Subscribing callback")
callbacks.add(listener)
if (callbacks.size == 1) {
serviceListing.setListening(true)
@@ -108,6 +134,7 @@ class ControlsListingControllerImpl @VisibleForTesting constructor(
*/
override fun removeCallback(listener: ControlsListingController.ControlsListingCallback) {
backgroundExecutor.execute {
Log.d(TAG, "Unsubscribing callback")
callbacks.remove(listener)
if (callbacks.size == 0) {
serviceListing.setListening(false)

View File

@@ -22,7 +22,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.settings.CurrentUserTracker
import com.android.systemui.util.LifecycleActivity
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -32,7 +35,9 @@ import javax.inject.Inject
*/
class ControlsProviderSelectorActivity @Inject constructor(
@Main private val executor: Executor,
private val listingController: ControlsListingController
@Background private val backExecutor: Executor,
private val listingController: ControlsListingController,
broadcastDispatcher: BroadcastDispatcher
) : LifecycleActivity() {
companion object {
@@ -40,6 +45,16 @@ class ControlsProviderSelectorActivity @Inject constructor(
}
private lateinit var recyclerView: RecyclerView
private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
private val startingUser = listingController.currentUserId
override fun onUserSwitched(newUserId: Int) {
if (newUserId != startingUser) {
stopTracking()
finish()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -50,6 +65,7 @@ class ControlsProviderSelectorActivity @Inject constructor(
recyclerView.layoutManager = LinearLayoutManager(applicationContext)
setContentView(recyclerView)
currentUserTracker.startTracking()
}
/**
@@ -57,13 +73,17 @@ class ControlsProviderSelectorActivity @Inject constructor(
* @param component a component name for a [ControlsProviderService]
*/
fun launchFavoritingActivity(component: ComponentName?) {
component?.let {
val intent = Intent(applicationContext, ControlsFavoritingActivity::class.java).apply {
putExtra(ControlsFavoritingActivity.EXTRA_APP, listingController.getAppLabel(it))
putExtra(ControlsFavoritingActivity.EXTRA_COMPONENT, it)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
backExecutor.execute {
component?.let {
val intent = Intent(applicationContext, ControlsFavoritingActivity::class.java)
.apply {
putExtra(ControlsFavoritingActivity.EXTRA_APP,
listingController.getAppLabel(it))
putExtra(ControlsFavoritingActivity.EXTRA_COMPONENT, it)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
startActivity(intent)
}
startActivity(intent)
}
}
}

View File

@@ -19,6 +19,7 @@ package com.android.systemui.controls.controller
import android.content.ComponentName
import android.content.Context
import android.os.Binder
import android.os.UserHandle
import android.service.controls.Control
import android.service.controls.DeviceTypes
import android.testing.AndroidTestingRunner
@@ -38,6 +39,7 @@ 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.verify
import org.mockito.MockitoAnnotations
@@ -55,6 +57,9 @@ class ControlsBindingControllerTest : SysuiTestCase() {
@Mock
private lateinit var mockControlsController: ControlsController
private val user = UserHandle.of(mContext.userId)
private val otherUser = UserHandle.of(user.identifier + 1)
private val executor = FakeExecutor(FakeSystemClock())
private lateinit var controller: ControlsBindingController
private val providers = TestableControlsBindingControllerImpl.providers
@@ -74,6 +79,11 @@ class ControlsBindingControllerTest : SysuiTestCase() {
providers.clear()
}
@Test
fun testStartOnUser() {
assertEquals(user.identifier, controller.currentUserId)
}
@Test
fun testBindAndLoad() {
val callback: (List<Control>) -> Unit = {}
@@ -145,6 +155,41 @@ class ControlsBindingControllerTest : SysuiTestCase() {
verify(it).unsubscribe()
}
}
@Test
fun testCurrentUserId() {
controller.changeUser(otherUser)
assertEquals(otherUser.identifier, controller.currentUserId)
}
@Test
fun testChangeUsers_providersHaveCorrectUser() {
controller.bindServices(listOf(TEST_COMPONENT_NAME_1))
controller.changeUser(otherUser)
controller.bindServices(listOf(TEST_COMPONENT_NAME_2))
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)
}
@Test
fun testChangeUsers_providersUnbound() {
controller.bindServices(listOf(TEST_COMPONENT_NAME_1))
controller.changeUser(otherUser)
val provider1 = providers.first { it.componentName == TEST_COMPONENT_NAME_1 }
verify(provider1).unbindService()
controller.bindServices(listOf(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()
}
}
class TestableControlsBindingControllerImpl(
@@ -157,12 +202,16 @@ class TestableControlsBindingControllerImpl(
val providers = mutableSetOf<ControlsProviderLifecycleManager>()
}
// Replaces the real provider with a mock and puts the mock in a visible set.
// The mock has the same componentName and user as the real one would have
override fun createProviderManager(component: ComponentName):
ControlsProviderLifecycleManager {
val realProvider = super.createProviderManager(component)
val provider = mock(ControlsProviderLifecycleManager::class.java)
val token = Binder()
`when`(provider.componentName).thenReturn(component)
`when`(provider.componentName).thenReturn(realProvider.componentName)
`when`(provider.token).thenReturn(token)
`when`(provider.user).thenReturn(realProvider.user)
providers.add(provider)
return provider
}

View File

@@ -17,7 +17,12 @@
package com.android.systemui.controls.controller
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.os.UserHandle
import android.provider.Settings
import android.service.controls.Control
import android.service.controls.DeviceTypes
@@ -26,6 +31,8 @@ import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.DumpController
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.util.concurrency.FakeExecutor
@@ -37,10 +44,11 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.eq
import org.mockito.Captor
import org.mockito.Mock
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.verify
@@ -61,18 +69,25 @@ class ControlsControllerImplTest : SysuiTestCase() {
private lateinit var pendingIntent: PendingIntent
@Mock
private lateinit var persistenceWrapper: ControlsFavoritePersistenceWrapper
@Mock
private lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock
private lateinit var listingController: ControlsListingController
@Captor
private lateinit var controlInfoListCaptor: ArgumentCaptor<List<ControlInfo>>
@Captor
private lateinit var controlLoadCallbackCaptor: ArgumentCaptor<(List<Control>) -> Unit>
@Captor
private lateinit var broadcastReceiverCaptor: ArgumentCaptor<BroadcastReceiver>
private lateinit var delayableExecutor: FakeExecutor
private lateinit var controller: ControlsController
companion object {
fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
fun <T : Any> safeEq(value: T): T = eq(value) ?: value
fun <T> eq(value: T): T = Mockito.eq(value) ?: value
fun <T> any(): T = Mockito.any<T>()
private val TEST_COMPONENT = ComponentName("test.pkg", "test.class")
private const val TEST_CONTROL_ID = "control1"
@@ -89,24 +104,39 @@ class ControlsControllerImplTest : SysuiTestCase() {
TEST_COMPONENT_2, TEST_CONTROL_ID_2, TEST_CONTROL_TITLE_2, TEST_DEVICE_TYPE_2)
}
private val user = mContext.userId
private val otherUser = user + 1
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
Settings.Secure.putInt(mContext.contentResolver,
ControlsControllerImpl.CONTROLS_AVAILABLE, 1)
Settings.Secure.putIntForUser(mContext.contentResolver,
ControlsControllerImpl.CONTROLS_AVAILABLE, 1, otherUser)
delayableExecutor = FakeExecutor(FakeSystemClock())
val wrapper = object : ContextWrapper(mContext) {
override fun createContextAsUser(user: UserHandle, flags: Int): Context {
return baseContext
}
}
controller = ControlsControllerImpl(
mContext,
wrapper,
delayableExecutor,
uiController,
bindingController,
listingController,
broadcastDispatcher,
Optional.of(persistenceWrapper),
dumpController
)
assertTrue(controller.available)
verify(broadcastDispatcher).registerReceiver(
capture(broadcastReceiverCaptor), any(), any(), eq(UserHandle.ALL))
}
private fun builderFromInfo(controlInfo: ControlInfo): Control.StatelessBuilder {
@@ -114,6 +144,11 @@ class ControlsControllerImplTest : SysuiTestCase() {
.setDeviceType(controlInfo.deviceType).setTitle(controlInfo.controlTitle)
}
@Test
fun testStartOnUser() {
assertEquals(user, controller.currentUserId)
}
@Test
fun testStartWithoutFavorites() {
assertTrue(controller.getFavoriteControls().isEmpty())
@@ -127,6 +162,8 @@ class ControlsControllerImplTest : SysuiTestCase() {
delayableExecutor,
uiController,
bindingController,
listingController,
broadcastDispatcher,
Optional.of(persistenceWrapper),
dumpController
)
@@ -190,7 +227,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
controller.loadForComponent(TEST_COMPONENT) {}
reset(persistenceWrapper)
verify(bindingController).bindAndLoad(safeEq(TEST_COMPONENT),
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
capture(controlLoadCallbackCaptor))
controlLoadCallbackCaptor.value.invoke(listOf(control))
@@ -262,7 +299,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
assertEquals(ControlStatus(control, false), controlStatus)
}
verify(bindingController).bindAndLoad(safeEq(TEST_COMPONENT),
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
capture(controlLoadCallbackCaptor))
controlLoadCallbackCaptor.value.invoke(listOf(control))
@@ -287,7 +324,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
assertEquals(ControlStatus(control2, false), controlStatus2)
}
verify(bindingController).bindAndLoad(safeEq(TEST_COMPONENT),
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
capture(controlLoadCallbackCaptor))
controlLoadCallbackCaptor.value.invoke(listOf(control, control2))
@@ -309,7 +346,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
assertTrue(controlStatus.removed)
}
verify(bindingController).bindAndLoad(safeEq(TEST_COMPONENT),
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
capture(controlLoadCallbackCaptor))
controlLoadCallbackCaptor.value.invoke(emptyList())
@@ -325,7 +362,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
controller.loadForComponent(TEST_COMPONENT) {}
verify(bindingController).bindAndLoad(safeEq(TEST_COMPONENT),
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
capture(controlLoadCallbackCaptor))
controlLoadCallbackCaptor.value.invoke(listOf(control))
@@ -358,4 +395,26 @@ class ControlsControllerImplTest : SysuiTestCase() {
controller.clearFavorites()
assertTrue(controller.getFavoriteControls().isEmpty())
}
}
@Test
fun testSwitchUsers() {
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
reset(persistenceWrapper)
val intent = Intent(Intent.ACTION_USER_SWITCHED).apply {
putExtra(Intent.EXTRA_USER_HANDLE, otherUser)
}
val pendingResult = mock(BroadcastReceiver.PendingResult::class.java)
`when`(pendingResult.sendingUserId).thenReturn(otherUser)
broadcastReceiverCaptor.value.pendingResult = pendingResult
broadcastReceiverCaptor.value.onReceive(mContext, intent)
verify(persistenceWrapper).changeFile(any())
verify(persistenceWrapper).readFavorites()
verify(bindingController).changeUser(UserHandle.of(otherUser))
verify(listingController).changeUser(UserHandle.of(otherUser))
assertTrue(controller.getFavoriteControls().isEmpty())
assertEquals(otherUser, controller.currentUserId)
}
}

View File

@@ -17,6 +17,7 @@
package com.android.systemui.controls.controller
import android.content.ComponentName
import android.os.UserHandle
import android.service.controls.Control
import android.service.controls.IControlsActionCallback
import android.service.controls.IControlsLoadCallback
@@ -86,6 +87,7 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
loadCallback,
actionCallback,
subscriber,
UserHandle.of(0),
componentName
)
}
@@ -148,4 +150,4 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
eq(actionCallback))
assertEquals(action, wrapperCaptor.getValue().getWrappedAction())
}
}
}

View File

@@ -17,12 +17,15 @@
package com.android.systemui.controls.management
import android.content.ComponentName
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.ServiceInfo
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.settingslib.applications.ServiceListing
import com.android.settingslib.widget.CandidateInfo
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import org.junit.After
@@ -69,13 +72,22 @@ class ControlsListingControllerImplTest : SysuiTestCase() {
private var serviceListingCallbackCaptor =
ArgumentCaptor.forClass(ServiceListing.Callback::class.java)
private val user = mContext.userId
private val otherUser = user + 1
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
`when`(serviceInfo.componentName).thenReturn(componentName)
controller = ControlsListingControllerImpl(mContext, executor, mockSL)
val wrapper = object : ContextWrapper(mContext) {
override fun createContextAsUser(user: UserHandle, flags: Int): Context {
return baseContext
}
}
controller = ControlsListingControllerImpl(wrapper, executor, { mockSL })
verify(mockSL).addCallback(capture(serviceListingCallbackCaptor))
}
@@ -85,6 +97,11 @@ class ControlsListingControllerImplTest : SysuiTestCase() {
executor.runAllReady()
}
@Test
fun testStartsOnUser() {
assertEquals(user, controller.currentUserId)
}
@Test
fun testNoServices_notListening() {
assertTrue(controller.getCurrentServices().isEmpty())
@@ -167,8 +184,9 @@ class ControlsListingControllerImplTest : SysuiTestCase() {
controller.addCallback(mockCallbackOther)
@Suppress("unchecked_cast")
val captor: ArgumentCaptor<List<CandidateInfo>> =
ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<CandidateInfo>>
val captor: ArgumentCaptor<List<ControlsServiceInfo>> =
ArgumentCaptor.forClass(List::class.java)
as ArgumentCaptor<List<ControlsServiceInfo>>
executor.runAllReady()
reset(mockCallback)
@@ -185,4 +203,11 @@ class ControlsListingControllerImplTest : SysuiTestCase() {
assertEquals(1, captor.value.size)
assertEquals(componentName.flattenToString(), captor.value[0].key)
}
@Test
fun testChangeUser() {
controller.changeUser(UserHandle.of(otherUser))
executor.runAllReady()
assertEquals(otherUser, controller.currentUserId)
}
}