From 313f37de55027fb6c67a4c3789c242229e0f3d4d Mon Sep 17 00:00:00 2001 From: Matt Pietal Date: Mon, 24 Feb 2020 11:27:22 -0500 Subject: [PATCH 1/2] Controls - Structure persistence Allow structure to be persisted and restored. Assume structure can be null in XML, to ease migration. Null values from Control will be mapped to an empty string in ControlInfo. Reduce binding controller to a single binding at a time. Bug: 148207527 Test: atest ControlsFavoritePersistenceWrapperTest ControlsControllerImplTest ControlsBindingControllerImplTest Change-Id: I220212666258311bd58b498d4d579977c11d3fae --- .../controls/controller/ControlInfo.kt | 46 +-- .../controller/ControlsBindingController.kt | 21 +- .../ControlsBindingControllerImpl.kt | 107 ++---- .../controls/controller/ControlsController.kt | 55 +-- .../controller/ControlsControllerImpl.kt | 346 ++++++++---------- .../ControlsFavoritePersistenceWrapper.kt | 63 ++-- .../ControlsProviderLifecycleManager.kt | 48 +-- .../controls/controller/StructureInfo.kt | 34 ++ .../management/ControlsFavoritingActivity.kt | 14 +- .../systemui/controls/ui/ControlViewHolder.kt | 2 +- .../systemui/controls/ui/ControlWithState.kt | 8 +- .../controls/ui/ControlsUiControllerImpl.kt | 54 ++- .../ControlsBindingControllerImplTest.kt | 87 ++--- .../controller/ControlsControllerImplTest.kt | 260 ++++++------- .../ControlsFavoritePersistenceWrapperTest.kt | 25 +- 15 files changed, 536 insertions(+), 634 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/controls/controller/StructureInfo.kt diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt index f62412069b92b..6f2af1bda45c2 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt @@ -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) } -} \ 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 07488ad836ef9..fd6e2566b1b6c 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt @@ -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) + 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) + 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) } -} \ No newline at end of file +} 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 a8981b06682a0..1d937ad75c994 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt @@ -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 = - ArrayMap() - @GuardedBy("componentMap") - private val componentMap: MutableMap = - ArrayMap() + private var currentProvider: ControlsProviderLifecycleManager? = null private val actionCallbackService = object : IControlsActionCallback.Stub() { override fun accept( @@ -108,83 +101,63 @@ 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) { - 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) { - 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) { @@ -203,20 +176,12 @@ 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 } private inner class OnLoadRunnable( @@ -233,11 +198,9 @@ open class ControlsBindingControllerImpl @Inject constructor( 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 - } + if (token != provider.token) { + Log.e(TAG, "Provider for token:$token does not exist anymore") + return } callback.accept(list) provider.unbindService() 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 4b89fd48972b3..f2881d41574c3 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -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 + fun getFavorites(): List /** * 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 + fun getFavoritesForComponent(componentName: ComponentName): List /** - * 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) - - /** - * 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]. */ 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 b97424985b5ab..22016a7750283 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -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>() - .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) } } @@ -185,9 +167,7 @@ class ControlsControllerImpl @Inject constructor ( init { dumpManager.registerDumpable(javaClass.name, this) - if (available) { - loadFavorites() - } + resetFavorites(available) userChanging = false broadcastDispatcher.registerReceiver( userSwitchReceiver, @@ -198,6 +178,14 @@ 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()) + } + } + private fun confirmAvailability(): Boolean { if (userChanging) { Log.w(TAG, "Controls not available while user is changing") @@ -210,16 +198,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 @@ -242,57 +220,55 @@ class ControlsControllerImpl @Inject constructor ( componentName, object : ControlsBindingController.LoadCallback { override fun accept(controls: List) { - val loadData = synchronized(currentFavorites) { - val favoritesForComponentKeys: List = - currentFavorites.getValue(componentName).map { it.controlId } - val changed = updateFavoritesLocked(componentName, controls, - favoritesForComponentKeys) - if (changed) { - persistenceWrapper.storeFavorites(favoritesAsListLocked()) - } - val removed = findRemovedLocked(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 favoritesForComponentKeys = Favorites + .getControlsForComponent(componentName).map { it.controlId } + + val changed = Favorites.updateControls(componentName, controls) + if (changed) { + persistenceWrapper.storeFavorites(Favorites.getAllStructures()) } + val removed = findRemovedLocked(favoritesForComponentKeys.toSet(), + controls) + val controlsWithFavorite = controls.map { + ControlStatus(it, it.controlId in favoritesForComponentKeys) + } + val loadData = createLoadDataObject( + Favorites.getControlsForComponent(componentName) + .filter { it.controlId in removed } + .map { createRemovedStatus(componentName, it) } + + controlsWithFavorite, + favoritesForComponentKeys + ) + 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, - true + Favorites.getControlsForComponent(componentName).let { controls -> + val keys = controls.map { it.controlId } + val loadData = createLoadDataObject( + controls.map { createRemovedStatus(componentName, it, false) }, + keys, + true ) + dataCallback.accept(loadData) } - dataCallback.accept(loadData) } } ) } 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 +278,15 @@ class ControlsControllerImpl @Inject constructor ( return ControlStatus(control, true, setRemoved) } - @GuardedBy("currentFavorites") private fun findRemovedLocked(favoriteKeys: Set, list: List): Set { val controlsKeys = list.map { it.controlId } return favoriteKeys.minus(controlsKeys) } - @GuardedBy("currentFavorites") - private fun updateFavoritesLocked( - componentName: ComponentName, - list: List, - favoriteKeys: List - ): 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 { - 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 +294,10 @@ 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() - } - if (changed) { - persistenceWrapper.storeFavorites(listOfControls) - } - } - - override fun replaceFavoritesForComponent( - componentName: ComponentName, - favorites: List - ) { - if (!confirmAvailability()) return - val filtered = favorites.filter { it.component == componentName } - val listOfControls = synchronized(currentFavorites) { - currentFavorites.put(componentName, filtered.toMutableList()) - favoritesAsListLocked() - } - persistenceWrapper.storeFavorites(listOfControls) + Favorites.replaceControls(structureInfo) + persistenceWrapper.storeFavorites(Favorites.getAllStructures()) } override fun refreshStatus(componentName: ComponentName, control: Control) { @@ -399,17 +306,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 +322,22 @@ class ControlsControllerImpl @Inject constructor ( uiController.onActionResponse(componentName, controlId, response) } - override fun getFavoriteControls(): List { - 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 = 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 { - return synchronized(currentFavorites) { - currentFavorites.get(componentName) ?: emptyList() - } - } + override fun getFavoritesForComponent(componentName: ComponentName): List = + Favorites.getStructuresForComponent(componentName) override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array) { pw.println("ControlsController state:") @@ -462,11 +345,106 @@ 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()) } -} \ No newline at end of file +} + +/** + * 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. + */ +private object Favorites { + private var favMap = mapOf>() + + fun getAllStructures(): List = favMap.flatMap { it.value } + + fun getStructuresForComponent(componentName: ComponentName): List = + favMap.get(componentName) ?: emptyList() + + fun getControlsForStructure(structure: StructureInfo): List = + getStructuresForComponent(structure.componentName) + .firstOrNull { it.structure == structure.structure } + ?.controls ?: emptyList() + + fun getControlsForComponent(componentName: ComponentName): List = + getStructuresForComponent(componentName).flatMap { it.controls } + + fun load(structures: List) { + favMap = structures.groupBy { it.componentName } + } + + fun updateControls(componentName: ComponentName, controls: List): Boolean { + val controlsById = controls.associateBy { it.controlId } + + // utilize a new map to allow for changes to structure names + val structureToControls = mutableMapOf>() + + // 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.toMap() + + return true + } + + fun replaceControls(updatedStructure: StructureInfo) { + val newFavMap = favMap.toMutableMap() + val structures = mutableListOf() + 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>() + } +} 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 883f8a93d9100..fe3f4f8ef07dc 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt @@ -45,6 +45,8 @@ 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" @@ -66,7 +68,7 @@ class ControlsFavoritePersistenceWrapper( * * @param list a list of favorite controls. The list will be stored in the same order. */ - fun storeFavorites(list: List) { + fun storeFavorites(structures: List) { executor.execute { Log.d(TAG, "Saving data to file: $file") val atomicFile = AtomicFile(file) @@ -81,16 +83,24 @@ 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_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 +119,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 { + fun readFavorites(): List { if (!file.exists()) { Log.d(TAG, "No favorites, returning empty list") return emptyList() @@ -134,25 +144,32 @@ class ControlsFavoritePersistenceWrapper( } } - private fun parseXml(parser: XmlPullParser): List { + private fun parseXml(parser: XmlPullParser): List { var type: Int - val infos = mutableListOf() + val infos = mutableListOf() + + var lastComponent: ComponentName? = null + var lastStructure: CharSequence? = null + var controls = mutableListOf() 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 } -} \ No newline at end of file +} 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 a53fcd498236a..86e8e834faf10 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt @@ -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) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/StructureInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/StructureInfo.kt new file mode 100644 index 0000000000000..34bfa135f0c85 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/StructureInfo.kt @@ -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 +) 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 2c014498fdc29..08a1a50001122 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt @@ -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