diff --git a/packages/SystemUI/res/drawable/controls_list_divider.xml b/packages/SystemUI/res/drawable/controls_list_divider.xml new file mode 100644 index 0000000000000..f8211d5297f3a --- /dev/null +++ b/packages/SystemUI/res/drawable/controls_list_divider.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_more_vert.xml b/packages/SystemUI/res/drawable/ic_more_vert.xml deleted file mode 100644 index 1309fa875b558..0000000000000 --- a/packages/SystemUI/res/drawable/ic_more_vert.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - diff --git a/packages/SystemUI/res/layout/controls_spinner_item.xml b/packages/SystemUI/res/layout/controls_spinner_item.xml new file mode 100644 index 0000000000000..6b880545b2960 --- /dev/null +++ b/packages/SystemUI/res/layout/controls_spinner_item.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml index 2cd9505b8fe40..77bcc3575fad8 100644 --- a/packages/SystemUI/res/layout/controls_with_favorites.xml +++ b/packages/SystemUI/res/layout/controls_with_favorites.xml @@ -14,44 +14,47 @@ ~ limitations under the License. --> + xmlns:android="http://schemas.android.com/apk/res/android"> - + android:paddingTop="12dp"> - + + android:layout_marginEnd="10dp" /> - + + + + + android:orientation="vertical" + android:paddingTop="20dp" /> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index aefe4a20a4962..016b48bb48a11 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1217,6 +1217,7 @@ 4dp + 1dp 15dp 100dp 15dp diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index d9b1452b6bb1c..83c5070955345 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -656,6 +656,12 @@ @*android:string/config_bodyFontFamily + + + 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..8f02c252beb1b 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,92 +101,70 @@ open class ControlsBindingControllerImpl @Inject constructor( } private fun retrieveLifecycleManager(component: ComponentName): - ControlsProviderLifecycleManager { - synchronized(componentMap) { - val provider = componentMap.getOrPut(Key(component, currentUser)) { - createProviderManager(component) - } - tokenMap.putIfAbsent(provider.token, provider) - return provider + ControlsProviderLifecycleManager? { + if (currentProvider != null && currentProvider?.componentName != component) { + unbind() } + + if (currentProvider == null) { + currentProvider = createProviderManager(component) + } + + return currentProvider } override fun bindAndLoad( component: ComponentName, callback: ControlsBindingController.LoadCallback ) { - val provider = retrieveLifecycleManager(component) - provider.maybeBindAndLoad(LoadSubscriber(callback)) + retrieveLifecycleManager(component)?.maybeBindAndLoad(LoadSubscriber(callback)) } - override fun subscribe(controls: List) { - 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) { backgroundExecutor.execute { - synchronized(componentMap) { - val removed = componentMap.remove(Key(componentName, currentUser)) - removed?.let { - it.unbindService() - tokenMap.remove(it.token) + currentProvider?.let { + if (it.componentName == componentName) { + unbind() } } } @@ -203,20 +174,31 @@ open class ControlsBindingControllerImpl @Inject constructor( return StringBuilder(" ControlsBindingController:\n").apply { append(" refreshing=${refreshing.get()}\n") append(" currentUser=$currentUser\n") - append(" Providers:\n") - synchronized(componentMap) { - componentMap.values.forEach { - append(" $it\n") - } - } + append(" Providers=$currentProvider\n") }.toString() } private abstract inner class CallbackRunnable(val token: IBinder) : Runnable { - protected val provider: ControlsProviderLifecycleManager? = - synchronized(componentMap) { - tokenMap.get(token) - } + protected val provider: ControlsProviderLifecycleManager? = currentProvider + + override fun run() { + if (provider == null) { + Log.e(TAG, "No current provider set") + return + } + if (provider.user != currentUser) { + Log.e(TAG, "User ${provider.user} is not current user") + return + } + if (token != provider.token) { + Log.e(TAG, "Provider for token:$token does not exist anymore") + return + } + + doRun() + } + + abstract fun doRun() } private inner class OnLoadRunnable( @@ -224,23 +206,9 @@ open class ControlsBindingControllerImpl @Inject constructor( val list: List, val callback: ControlsBindingController.LoadCallback ) : CallbackRunnable(token) { - override fun run() { - if (provider == null) { - Log.e(TAG, "No provider found for token:$token") - return - } - if (provider.user != currentUser) { - Log.e(TAG, "User ${provider.user} is not current user") - return - } - synchronized(componentMap) { - if (token !in tokenMap.keys) { - Log.e(TAG, "Provider for token:$token does not exist anymore") - return - } - } + override fun doRun() { callback.accept(list) - provider.unbindService() + provider?.unbindService() } } @@ -248,14 +216,11 @@ open class ControlsBindingControllerImpl @Inject constructor( token: IBinder, val control: Control ) : CallbackRunnable(token) { - override fun run() { + override fun doRun() { if (!refreshing.get()) { Log.d(TAG, "onRefresh outside of window from:${provider?.componentName}") } - if (provider?.user != currentUser) { - Log.e(TAG, "User ${provider?.user} is not current user") - return - } + provider?.let { lazyController.get().refreshStatus(it.componentName, control) } @@ -266,7 +231,7 @@ open class ControlsBindingControllerImpl @Inject constructor( token: IBinder, val subscription: IControlsSubscription ) : CallbackRunnable(token) { - override fun run() { + override fun doRun() { if (!refreshing.get()) { Log.d(TAG, "onRefresh outside of window from '${provider?.componentName}'") } @@ -279,7 +244,7 @@ open class ControlsBindingControllerImpl @Inject constructor( private inner class OnCompleteRunnable( token: IBinder ) : CallbackRunnable(token) { - override fun run() { + override fun doRun() { provider?.let { Log.i(TAG, "onComplete receive from '${it.componentName}'") } @@ -290,7 +255,7 @@ open class ControlsBindingControllerImpl @Inject constructor( token: IBinder, val error: String ) : CallbackRunnable(token) { - override fun run() { + override fun doRun() { provider?.let { Log.e(TAG, "onError receive from '${it.componentName}': $error") } @@ -302,11 +267,7 @@ open class ControlsBindingControllerImpl @Inject constructor( val controlId: String, @ControlAction.ResponseResult val response: Int ) : CallbackRunnable(token) { - override fun run() { - if (provider?.user != currentUser) { - Log.e(TAG, "User ${provider?.user} is not current user") - return - } + override fun doRun() { provider?.let { lazyController.get().onActionResponse(it.componentName, controlId, response) } @@ -318,7 +279,7 @@ open class ControlsBindingControllerImpl @Inject constructor( val error: String, val callback: ControlsBindingController.LoadCallback ) : CallbackRunnable(token) { - override fun run() { + override fun doRun() { callback.error(error) provider?.let { Log.e(TAG, "onError receive from '${it.componentName}': $error") 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..dedd341a46beb 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) } } @@ -163,21 +145,23 @@ class ControlsControllerImpl @Inject constructor ( * If some component has been removed, the new set of favorites will also be saved. */ private val listingCallback = object : ControlsListingController.ControlsListingCallback { - override fun onServicesUpdated(candidates: List) { + override fun onServicesUpdated(serviceInfos: List) { executor.execute { - val candidateComponents = candidates.map(ControlsServiceInfo::componentName) - synchronized(currentFavorites) { - val components = currentFavorites.keys.toSet() // create a copy - components.forEach { - if (it !in candidateComponents) { - currentFavorites.remove(it) - bindingController.onComponentRemoved(it) - } - } - // Check if something has been removed, if so, store the new list - if (components.size > currentFavorites.size) { - persistenceWrapper.storeFavorites(favoritesAsListLocked()) - } + val serviceInfoSet = serviceInfos.map(ControlsServiceInfo::componentName).toSet() + val favoriteComponentSet = Favorites.getAllStructures().map { + it.componentName + }.toSet() + + var changed = false + favoriteComponentSet.subtract(serviceInfoSet).forEach { + changed = true + Favorites.removeStructures(it) + bindingController.onComponentRemoved(it) + } + + // Check if something has been removed, if so, store the new list + if (changed) { + persistenceWrapper.storeFavorites(Favorites.getAllStructures()) } } } @@ -185,9 +169,7 @@ class ControlsControllerImpl @Inject constructor ( init { dumpManager.registerDumpable(javaClass.name, this) - if (available) { - loadFavorites() - } + resetFavorites(available) userChanging = false broadcastDispatcher.registerReceiver( userSwitchReceiver, @@ -198,6 +180,15 @@ class ControlsControllerImpl @Inject constructor ( contentResolver.registerContentObserver(URI, false, settingObserver, UserHandle.USER_ALL) } + private fun resetFavorites(shouldLoad: Boolean) { + Favorites.clear() + + if (shouldLoad) { + Favorites.load(persistenceWrapper.readFavorites()) + listingController.addCallback(listingCallback) + } + } + private fun confirmAvailability(): Boolean { if (userChanging) { Log.w(TAG, "Controls not available while user is changing") @@ -210,16 +201,6 @@ class ControlsControllerImpl @Inject constructor ( return true } - private fun loadFavorites() { - val infos = persistenceWrapper.readFavorites() - synchronized(currentFavorites) { - infos.forEach { - currentFavorites.getOrPut(it.component, { mutableListOf() }).add(it) - } - } - listingController.addCallback(listingCallback) - } - override fun loadForComponent( componentName: ComponentName, dataCallback: Consumer @@ -242,41 +223,41 @@ 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) + executor.execute { + val favoritesForComponentKeys = Favorites + .getControlsForComponent(componentName).map { it.controlId } + + val changed = Favorites.updateControls(componentName, controls) if (changed) { - persistenceWrapper.storeFavorites(favoritesAsListLocked()) + persistenceWrapper.storeFavorites(Favorites.getAllStructures()) } - val removed = findRemovedLocked(favoritesForComponentKeys.toSet(), - controls) + val removed = findRemoved(favoritesForComponentKeys.toSet(), controls) val controlsWithFavorite = controls.map { ControlStatus(it, it.controlId in favoritesForComponentKeys) } - createLoadDataObject( - currentFavorites.getValue(componentName) - .filter { it.controlId in removed } - .map { createRemovedStatus(it) } + - controlsWithFavorite, - favoritesForComponentKeys + val loadData = createLoadDataObject( + Favorites.getControlsForComponent(componentName) + .filter { it.controlId in removed } + .map { createRemovedStatus(componentName, it) } + + controlsWithFavorite, + favoritesForComponentKeys ) + + dataCallback.accept(loadData) } - dataCallback.accept(loadData) } override fun error(message: String) { - val loadData = synchronized(currentFavorites) { - val favoritesForComponent = currentFavorites.getValue(componentName) - val favoritesForComponentKeys = favoritesForComponent - .map { it.controlId } - createLoadDataObject( - favoritesForComponent.map { createRemovedStatus(it, false) }, - favoritesForComponentKeys, + val loadData = Favorites.getControlsForComponent(componentName).let { + controls -> + val keys = controls.map { it.controlId } + createLoadDataObject( + controls.map { createRemovedStatus(componentName, it, false) }, + keys, true - ) + ) } + dataCallback.accept(loadData) } } @@ -284,15 +265,16 @@ class ControlsControllerImpl @Inject constructor ( } private fun createRemovedStatus( + componentName: ComponentName, controlInfo: ControlInfo, setRemoved: Boolean = true ): ControlStatus { val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) - this.`package` = controlInfo.component.packageName + this.`package` = componentName.packageName } val pendingIntent = PendingIntent.getActivity(context, - controlInfo.component.hashCode(), + componentName.hashCode(), intent, 0) val control = Control.StatelessBuilder(controlInfo.controlId, pendingIntent) @@ -302,50 +284,15 @@ class ControlsControllerImpl @Inject constructor ( return ControlStatus(control, true, setRemoved) } - @GuardedBy("currentFavorites") - private fun findRemovedLocked(favoriteKeys: Set, list: List): Set { + private fun findRemoved(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 +300,12 @@ class ControlsControllerImpl @Inject constructor ( bindingController.unsubscribe() } - override fun changeFavoriteStatus(controlInfo: ControlInfo, state: Boolean) { + override fun replaceFavoritesForStructure(structureInfo: StructureInfo) { if (!confirmAvailability()) return - var changed = false - val listOfControls = synchronized(currentFavorites) { - if (state) { - if (controlInfo.component !in currentFavorites) { - currentFavorites.put(controlInfo.component, mutableListOf()) - changed = true - } - val controlsForComponent = currentFavorites.getValue(controlInfo.component) - if (controlsForComponent.firstOrNull { - it.controlId == controlInfo.controlId - } == null) { - controlsForComponent.add(controlInfo) - changed = true - } - } else { - changed = currentFavorites.get(controlInfo.component) - ?.remove(controlInfo) != null - } - favoritesAsListLocked() + executor.execute { + Favorites.replaceControls(structureInfo) + persistenceWrapper.storeFavorites(Favorites.getAllStructures()) } - if (changed) { - persistenceWrapper.storeFavorites(listOfControls) - } - } - - override fun replaceFavoritesForComponent( - componentName: ComponentName, - favorites: List - ) { - if (!confirmAvailability()) return - val filtered = favorites.filter { it.component == componentName } - val listOfControls = synchronized(currentFavorites) { - currentFavorites.put(componentName, filtered.toMutableList()) - favoritesAsListLocked() - } - persistenceWrapper.storeFavorites(listOfControls) } override fun refreshStatus(componentName: ComponentName, control: Control) { @@ -399,17 +314,12 @@ class ControlsControllerImpl @Inject constructor ( return } executor.execute { - synchronized(currentFavorites) { - val favoriteKeysForComponent = - currentFavorites.get(componentName)?.map { it.controlId } ?: emptyList() - val changed = updateFavoritesLocked( - componentName, - listOf(control), - favoriteKeysForComponent - ) - if (changed) { - persistenceWrapper.storeFavorites(favoritesAsListLocked()) - } + val changed = Favorites.updateControls( + componentName, + listOf(control) + ) + if (changed) { + persistenceWrapper.storeFavorites(Favorites.getAllStructures()) } } uiController.onRefreshState(componentName, listOf(control)) @@ -420,41 +330,22 @@ class ControlsControllerImpl @Inject constructor ( uiController.onActionResponse(componentName, controlId, response) } - override fun getFavoriteControls(): List { - 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 +353,114 @@ class ControlsControllerImpl @Inject constructor ( pw.println(" Changing users: $userChanging") pw.println(" Current user: ${currentUser.identifier}") pw.println(" Favorites:") - synchronized(currentFavorites) { - favoritesAsListLocked().forEach { - pw.println(" ${ it }") + Favorites.getAllStructures().forEach { s -> + pw.println(" ${ s }") + s.controls.forEach { c -> + pw.println(" ${ c }") } } pw.println(bindingController.toString()) } -} \ 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. + * + * Update/replace calls should use thread isolation to avoid race conditions. + */ +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 + + return true + } + + fun removeStructures(componentName: ComponentName) { + val newFavMap = favMap.toMutableMap() + newFavMap.remove(componentName) + favMap = newFavMap + } + + fun replaceControls(updatedStructure: StructureInfo) { + val newFavMap = favMap.toMutableMap() + val structures = mutableListOf() + 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..4bea6ef3d7b9a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt @@ -45,11 +45,17 @@ class ControlsFavoritePersistenceWrapper( private const val TAG = "ControlsFavoritePersistenceWrapper" const val FILE_NAME = "controls_favorites.xml" private const val TAG_CONTROLS = "controls" + private const val TAG_STRUCTURES = "structures" + private const val TAG_STRUCTURE = "structure" private const val TAG_CONTROL = "control" private const val TAG_COMPONENT = "component" private const val TAG_ID = "id" private const val TAG_TITLE = "title" private const val TAG_TYPE = "type" + private const val TAG_VERSION = "version" + + // must increment with every change to the XML structure + private const val VERSION = 1 } /** @@ -66,7 +72,7 @@ class ControlsFavoritePersistenceWrapper( * * @param list a list of favorite controls. The list will be stored in the same order. */ - fun storeFavorites(list: List) { + fun storeFavorites(structures: List) { executor.execute { Log.d(TAG, "Saving data to file: $file") val atomicFile = AtomicFile(file) @@ -81,16 +87,28 @@ class ControlsFavoritePersistenceWrapper( setOutput(writer, "utf-8") setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true) startDocument(null, true) - startTag(null, TAG_CONTROLS) - list.forEach { - startTag(null, TAG_CONTROL) - attribute(null, TAG_COMPONENT, it.component.flattenToString()) - attribute(null, TAG_ID, it.controlId) - attribute(null, TAG_TITLE, it.controlTitle.toString()) - attribute(null, TAG_TYPE, it.deviceType.toString()) - endTag(null, TAG_CONTROL) + startTag(null, TAG_VERSION) + text("$VERSION") + endTag(null, TAG_VERSION) + + startTag(null, TAG_STRUCTURES) + structures.forEach { s -> + startTag(null, TAG_STRUCTURE) + attribute(null, TAG_COMPONENT, s.componentName.flattenToString()) + attribute(null, TAG_STRUCTURE, s.structure.toString()) + + startTag(null, TAG_CONTROLS) + s.controls.forEach { c -> + startTag(null, TAG_CONTROL) + attribute(null, TAG_ID, c.controlId) + attribute(null, TAG_TITLE, c.controlTitle.toString()) + attribute(null, TAG_TYPE, c.deviceType.toString()) + endTag(null, TAG_CONTROL) + } + endTag(null, TAG_CONTROLS) + endTag(null, TAG_STRUCTURE) } - endTag(null, TAG_CONTROLS) + endTag(null, TAG_STRUCTURES) endDocument() atomicFile.finishWrite(writer) } @@ -109,7 +127,7 @@ class ControlsFavoritePersistenceWrapper( * @return a list of stored favorite controls. Return an empty list if the file is not found * @throws [IllegalStateException] if there is an error while reading the file */ - fun readFavorites(): List { + fun readFavorites(): List { if (!file.exists()) { Log.d(TAG, "No favorites, returning empty list") return emptyList() @@ -134,25 +152,32 @@ class ControlsFavoritePersistenceWrapper( } } - private fun parseXml(parser: XmlPullParser): List { + 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/AppAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt index 855e4a501f0a3..8b3454a2bc7c1 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt @@ -26,15 +26,13 @@ import android.widget.TextView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView -import com.android.settingslib.applications.DefaultAppInfo -import com.android.settingslib.widget.CandidateInfo import com.android.systemui.R import com.android.systemui.controls.ControlsServiceInfo import java.text.Collator import java.util.concurrent.Executor /** - * Adapter for binding [CandidateInfo] related to [ControlsProviderService]. + * Adapter for binding [ControlsServiceInfo] related to [ControlsProviderService]. * * This class handles subscribing and keeping track of the list of valid applications for * displaying. @@ -56,16 +54,16 @@ class AppAdapter( private val resources: Resources ) : RecyclerView.Adapter() { - private var listOfServices = emptyList() + private var listOfServices = emptyList() private val callback = object : ControlsListingController.ControlsListingCallback { - override fun onServicesUpdated(candidates: List) { + override fun onServicesUpdated(serviceInfos: List) { backgroundExecutor.execute { val collator = Collator.getInstance(resources.configuration.locales[0]) - val localeComparator = compareBy(collator) { + val localeComparator = compareBy(collator) { it.loadLabel() } - listOfServices = candidates.sortedWith(localeComparator) + listOfServices = serviceInfos.sortedWith(localeComparator) uiExecutor.execute(::notifyDataSetChanged) } } @@ -101,11 +99,10 @@ class AppAdapter( * Bind data to the view * @param data Information about the [ControlsProviderService] to bind to the data */ - fun bindData(data: CandidateInfo) { + fun bindData(data: ControlsServiceInfo) { icon.setImageDrawable(data.loadIcon()) title.text = data.loadLabel() - favorites.text = favRenderer.renderFavoritesForComponent( - (data as DefaultAppInfo).componentName) + favorites.text = favRenderer.renderFavoritesForComponent(data.componentName) } } } @@ -123,4 +120,4 @@ class FavoritesRenderer( return "" } } -} \ No newline at end of file +} 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