diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 26fa1cf469749..149eaf47b97d5 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -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"> diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt index e6cdf50580d86..53841e2f144b8 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt @@ -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) \ No newline at end of file +data class ControlStatus( + val control: Control, + val favorite: Boolean, + val removed: Boolean = false +) \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt index 265ddd8043b66..588ef5c4e68f5 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt @@ -22,7 +22,7 @@ import com.android.settingslib.applications.DefaultAppInfo class ControlsServiceInfo( context: Context, - serviceInfo: ServiceInfo + val serviceInfo: ServiceInfo ) : DefaultAppInfo( context, context.packageManager, diff --git a/packages/SystemUI/src/com/android/systemui/controls/UserAwareController.kt b/packages/SystemUI/src/com/android/systemui/controls/UserAwareController.kt new file mode 100644 index 0000000000000..4f39f2255a759 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/UserAwareController.kt @@ -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 +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt index 6b7fc4b7e8271..12c3ce9c69ee3 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt @@ -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) -> Unit) fun bindServices(components: List) fun subscribe(controls: List) diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt index 2db2cf1af191e..48d2fa68d1983 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt @@ -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 = ArrayMap() @GuardedBy("componentMap") - private val componentMap: MutableMap = - ArrayMap() + private val componentMap: MutableMap = + ArrayMap() private val loadCallbackService = object : IControlsLoadCallback.Stub() { override fun accept(token: IBinder, controls: MutableList) { @@ -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) \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt index e098faa00d038..b02de4500043a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index d5b5b5f0442eb..6ff1cf8474d34 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -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, + private val listingController: ControlsListingController, + broadcastDispatcher: BroadcastDispatcher, + optionalWrapper: Optional, 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>() + + 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>() - - 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) -> 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 { - 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) { 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()) } -} +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt index 6f2d71fd0f59a..7d1df14bbf1b8 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt @@ -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) { 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 { - var type: Int = 0 + var type = 0 val infos = mutableListOf() 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)) } } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt index 99aa3601ba301..739ca7ec0c29a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt @@ -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 } } -} +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt index d62bb4def3aa1..22c69086cf8c2 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt index 01c4fef67fd48..7ee4fd5b059e5 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt @@ -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(EXTRA_COMPONENT) + component = intent.getParcelableExtra(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 { diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt index 09e0ce9fea8d9..34db684022fb0 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt @@ -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 { + CallbackController, + UserAwareController { fun getCurrentServices(): List fun getAppLabel(name: ComponentName): CharSequence? = "" + @FunctionalInterface interface ControlsListingCallback { fun onServicesUpdated(list: List) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt index 3949c5929a85e..882382cc4ade7 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt @@ -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() - 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() @@ -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) diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt index 69af516b4ac92..5ff949c98806a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt @@ -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) } } } \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt index 7c8c7c8f7be6d..c1ebae735c7a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt @@ -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) -> 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() } + // 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 } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt index be86a9c15e5f7..897091f69f36a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt @@ -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> @Captor private lateinit var controlLoadCallbackCaptor: ArgumentCaptor<(List) -> Unit> + @Captor + private lateinit var broadcastReceiverCaptor: ArgumentCaptor private lateinit var delayableExecutor: FakeExecutor private lateinit var controller: ControlsController companion object { fun capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture() - fun safeEq(value: T): T = eq(value) ?: value + fun eq(value: T): T = Mockito.eq(value) ?: value + fun any(): T = Mockito.any() 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) + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt index 4fc1cca76be65..3f1435be8b3f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt @@ -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()) } -} +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt index f09aab97a2190..85e937e40acd4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt @@ -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> = - ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor> + val captor: ArgumentCaptor> = + ArgumentCaptor.forClass(List::class.java) + as ArgumentCaptor> 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) + } } \ No newline at end of file