diff --git a/packages/SystemUI/res/layout/controls_management_favorites.xml b/packages/SystemUI/res/layout/controls_management_favorites.xml index a36dd1247a04e..62056e60372d8 100644 --- a/packages/SystemUI/res/layout/controls_management_favorites.xml +++ b/packages/SystemUI/res/layout/controls_management_favorites.xml @@ -21,15 +21,32 @@ android:layout_height="match_parent" android:orientation="vertical"> + + Controls Choose controls for quick access + + Favorites + + All + + The list of all controls could not be loaded. diff --git a/packages/SystemUI/src/com/android/systemui/controls/UserAwareController.kt b/packages/SystemUI/src/com/android/systemui/controls/UserAwareController.kt index 4f39f2255a759..d2776d27ae62c 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/UserAwareController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/UserAwareController.kt @@ -18,8 +18,11 @@ package com.android.systemui.controls import android.os.UserHandle +/** + * An interface for controllers that keep track of the current user and can be notified of user + * changes. + */ 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/ControlInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt index b6cca3f5b4e10..f62412069b92b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt @@ -22,6 +22,15 @@ 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 + * 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 controlTitle last title reported for this [Control]. + * @property deviceType last reported type for this [Control]. */ data class ControlInfo( val component: ComponentName, @@ -33,6 +42,14 @@ data class ControlInfo( 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]) @@ -53,6 +70,12 @@ data class ControlInfo( if (DeviceTypes.validDeviceType(type)) type else DeviceTypes.TYPE_UNKNOWN) } } + + /** + * Returns a [String] representation of the fields separated using [SEPARATOR]. + * + * @return a [String] representation of `this` + */ override fun toString(): String { return component.flattenToString() + "$SEPARATOR$controlId$SEPARATOR$controlTitle$SEPARATOR$deviceType" 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 12c3ce9c69ee3..7fae6a3249c65 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt @@ -18,13 +18,72 @@ package com.android.systemui.controls.controller import android.content.ComponentName import android.service.controls.Control +import android.service.controls.ControlsProviderService import android.service.controls.actions.ControlAction import com.android.systemui.controls.UserAwareController +import java.util.function.Consumer +/** + * Controller for keeping track of any [ControlsProviderService] that needs to be bound. + * + * This controller serves as an interface between [ControlsController] and the services. + * + * This controller being a [UserAwareController] means that all binding and requests will be + * performed on services bound as the current user. + */ interface ControlsBindingController : UserAwareController { - fun bindAndLoad(component: ComponentName, callback: (List) -> Unit) + + /** + * Request bind to a service and load all controls. + * + * @param component The [ComponentName] of the service to bind + * @param callback a callback to return the loaded controls to (or an error). + */ + fun bindAndLoad(component: ComponentName, callback: LoadCallback) + + /** + * Request to bind to the given services. + * + * @param components a list of [ComponentName] of the services to bind + */ fun bindServices(components: List) + + /** + * 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 + */ fun subscribe(controls: List) + + /** + * Send an action performed on a [Control]. + * + * @param controlInfo information about the actioned control, including the [ComponentName] + * @param action the action performed on the control + */ fun action(controlInfo: ControlInfo, action: ControlAction) + + /** + * Unsubscribe from all services to stop status updates. + */ fun unsubscribe() + + /** + * Consumer for load calls. + * + * Supports also sending error messages. + */ + interface LoadCallback : Consumer> { + + /** + * Indicates an error loading. + * + * @message an error message. + */ + 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 0a2a9255c3eac..87bdfa8e04ec3 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt @@ -125,7 +125,10 @@ open class ControlsBindingControllerImpl @Inject constructor( } } - override fun bindAndLoad(component: ComponentName, callback: (List) -> Unit) { + override fun bindAndLoad( + component: ComponentName, + callback: ControlsBindingController.LoadCallback + ) { val provider = retrieveLifecycleManager(component) provider.maybeBindAndLoad(callback) } @@ -230,7 +233,7 @@ open class ControlsBindingControllerImpl @Inject constructor( return } } - provider.lastLoadCallback?.invoke(list) ?: run { + provider.lastLoadCallback?.accept(list) ?: run { Log.w(TAG, "Null callback") } 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 fce504120b624..4b89fd48972b3 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -18,33 +18,194 @@ package com.android.systemui.controls.controller import android.content.ComponentName import android.service.controls.Control +import android.service.controls.ControlsProviderService import android.service.controls.actions.ControlAction import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.UserAwareController +import com.android.systemui.controls.management.ControlsFavoritingActivity +import com.android.systemui.controls.ui.ControlsUiController +import java.util.function.Consumer +/** + * Controller to handle communication between different parts of the controls system. + * + * This controller is in charge of: + * * Keeping track of favorites + * * Determining and keeping track of whether controls are enabled + * * Listening for user change and propagating that message in the system + * * Communicate between the UI and the [ControlsBindingController] + * + * This controller being a [UserAwareController] means that all operations will be conducted on + * information for the current user only. + */ interface ControlsController : UserAwareController { + + /** + * Whether the controls system is available for the current user. + */ val available: Boolean - fun getFavoriteControls(): List + // SERVICE COMMUNICATION + + /** + * Load all available [Control] for a given service. + * + * @param componentName the [ComponentName] of the [ControlsProviderService] to load from + * @param dataCallback a callback in which to retrieve the result. + */ fun loadForComponent( componentName: ComponentName, - callback: (List, List) -> Unit + dataCallback: Consumer ) + /** + * Request to subscribe for all favorite controls. + * + * @see [ControlsBindingController.subscribe] + */ fun subscribeToFavorites() - fun changeFavoriteStatus(controlInfo: ControlInfo, state: Boolean) - fun replaceFavoritesForComponent(componentName: ComponentName, favorites: List) - - fun getFavoritesForComponent(componentName: ComponentName): List - fun countFavoritesForComponent(componentName: ComponentName): Int + /** + * Request to unsubscribe to all providers. + * + * @see [ControlsBindingController.unsubscribe] + */ fun unsubscribe() + + /** + * Notify a [ControlsProviderService] that an action has been performed on a [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) + + /** + * Refresh the status of a [Control] with information provided from the service. + * + * @param componentName the name of the service that provides the [Control] + * @param control a stateful [Control] with updated information + * @see [ControlsUiController.onRefreshState] + */ fun refreshStatus(componentName: ComponentName, control: Control) + + /** + * Indicate the result of a [ControlAction] performed on a [Control]. + * + * @param componentName the name of the service that provides the [Control] + * @param controlId the id of the [Control] the actioned was performed on + * @param response the result of the action. + * @see [ControlsUiController.onActionResponse] + */ fun onActionResponse( componentName: ComponentName, controlId: String, @ControlAction.ResponseResult response: Int ) + + // FAVORITE MANAGEMENT + + /** + * Get a list of all favorite controls. + * + * @return a list of [ControlInfo] with persistent information about the controls, including + * their corresponding [ComponentName]. + */ + fun getFavoriteControls(): 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. + */ + fun getFavoritesForComponent(componentName: ComponentName): List + + /** + * Replaces the favorites for the given component. + * + * 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. + */ + 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) + + /** + * Return the number of favorites for a given component. + * + * This call returns the same as `getFavoritesForComponent(componentName).size`. + * + * @param componentName the name of the component + * @return the number of current favorites for the given component + */ + 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]. + */ + interface LoadData { + /** + * All of the available controls for the loaded [ControlsProviderService]. + * + * This will indicate if they are currently a favorite and whether they were removed (a + * favorite but not retrieved on load). + */ + val allControls: List + + /** + * Ordered list of ids of favorite controls. + */ + val favoritesIds: List + + /** + * Whether there was an error in loading. + * + * In this case, [allControls] will only contain those that were favorited and will not be + * marked as removed. + */ + val errorOnLoad: Boolean + } +} + +/** + * Creates a basic implementation of a [LoadData]. + */ +fun createLoadDataObject( + allControls: List, + favorites: List, + error: Boolean = false +): ControlsController.LoadData { + return object : ControlsController.LoadData { + override val allControls = allControls + override val favoritesIds = favorites + override val errorOnLoad = error + } } 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 e611197b78ae8..50ad515f0ae04 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -38,7 +38,6 @@ 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 @@ -47,6 +46,7 @@ import java.io.FileDescriptor import java.io.PrintWriter import java.util.Optional import java.util.concurrent.TimeUnit +import java.util.function.Consumer import javax.inject.Inject import javax.inject.Singleton @@ -188,47 +188,74 @@ class ControlsControllerImpl @Inject constructor ( override fun loadForComponent( componentName: ComponentName, - callback: (List, List) -> Unit + dataCallback: Consumer ) { if (!confirmAvailability()) { if (userChanging) { // Try again later, userChanging should not last forever. If so, we have bigger // problems executor.executeDelayed( - { loadForComponent(componentName, callback) }, + { loadForComponent(componentName, dataCallback) }, USER_CHANGE_RETRY_DELAY, TimeUnit.MILLISECONDS ) } else { - callback(emptyList(), emptyList()) + dataCallback.accept(createLoadDataObject(emptyList(), emptyList(), true)) } return } - bindingController.bindAndLoad(componentName) { - synchronized(currentFavorites) { - val favoritesForComponentKeys: List = - currentFavorites.getValue(componentName).map { it.controlId } - val changed = updateFavoritesLocked(componentName, it, favoritesForComponentKeys) - if (changed) { - persistenceWrapper.storeFavorites(favoritesAsListLocked()) + bindingController.bindAndLoad( + 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 + ) + } + 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 + ) + } + dataCallback.accept(loadData) + } } - val removed = findRemovedLocked(favoritesForComponentKeys.toSet(), it) - val controlsWithFavorite = - it.map { ControlStatus(it, it.controlId in favoritesForComponentKeys) } - callback( - currentFavorites.getValue(componentName) - .filter { it.controlId in removed } - .map(::createRemovedStatus) + controlsWithFavorite, - favoritesForComponentKeys - ) - } - } + ) } - private fun createRemovedStatus(controlInfo: ControlInfo): ControlStatus { - val intent = Intent(context, ControlsFavoritingActivity::class.java).apply { - putExtra(Intent.EXTRA_COMPONENT_NAME, controlInfo.component) - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP + private fun createRemovedStatus( + controlInfo: ControlInfo, + setRemoved: Boolean = true + ): ControlStatus { + val intent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_LAUNCHER) + this.`package` = controlInfo.component.packageName } val pendingIntent = PendingIntent.getActivity(context, controlInfo.component.hashCode(), @@ -238,7 +265,7 @@ class ControlsControllerImpl @Inject constructor ( .setTitle(controlInfo.controlTitle) .setDeviceType(controlInfo.deviceType) .build() - return ControlStatus(control, true, true) + return ControlStatus(control, true, setRemoved) } @GuardedBy("currentFavorites") 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 7d1df14bbf1b8..d7f3c73a0bec0 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt @@ -19,9 +19,7 @@ package com.android.systemui.controls.controller import android.content.ComponentName import android.util.AtomicFile import android.util.Log -import android.util.Slog import android.util.Xml -import com.android.systemui.util.concurrency.DelayableExecutor import libcore.io.IoUtils import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException @@ -29,10 +27,18 @@ import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.io.IOException +import java.util.concurrent.Executor +/** + * Manages persistence of favorite controls. + * + * This class uses an [AtomicFile] to serialize the favorite controls to an xml. + * @property file a file location for storing/reading the favorites. + * @property executor an executor in which to execute storing the favorites. + */ class ControlsFavoritePersistenceWrapper( private var file: File, - private var executor: DelayableExecutor + private val executor: Executor ) { companion object { @@ -46,10 +52,20 @@ class ControlsFavoritePersistenceWrapper( private const val TAG_TYPE = "type" } + /** + * Change the file location for storing/reading the favorites + * + * @param fileName new location + */ fun changeFile(fileName: File) { file = fileName } + /** + * Stores the list of favorites in the corresponding file. + * + * @param list a list of favorite controls. The list will be stored in the same order. + */ fun storeFavorites(list: List) { executor.execute { Log.d(TAG, "Saving data to file: $file") @@ -87,6 +103,12 @@ class ControlsFavoritePersistenceWrapper( } } + /** + * Stores the list of favorites in the corresponding file. + * + * @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 { if (!file.exists()) { Log.d(TAG, "No favorites, returning empty list") @@ -95,7 +117,7 @@ class ControlsFavoritePersistenceWrapper( val reader = try { FileInputStream(file) } catch (fnfe: FileNotFoundException) { - Slog.i(TAG, "No file found") + Log.i(TAG, "No file found") return emptyList() } try { 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 b4bd82c84e1a5..e09d20b5069e9 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt @@ -25,7 +25,7 @@ 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 import android.service.controls.ControlsProviderService.CALLBACK_BUNDLE import android.service.controls.ControlsProviderService.CALLBACK_TOKEN import android.service.controls.IControlsActionCallback @@ -40,7 +40,23 @@ import com.android.internal.annotations.GuardedBy import com.android.systemui.util.concurrency.DelayableExecutor import java.util.concurrent.TimeUnit -typealias LoadCallback = (List) -> Unit +/** + * Manager for the lifecycle of the connection to a given [ControlsProviderService]. + * + * This class handles binding and unbinding and requests to the service. The class will queue + * requests until the service is connected and dispatch them then. + * + * @property context A SystemUI context for binding to the services + * @property executor A delayable executor for posting timeouts + * @property loadCallbackService a callback interface to hand the remote service for loading + * controls + * @property actionCallbackService a callback interface to hand the remote service for sending + * action responses + * @property subscriberService an "subscriber" interface for requesting and accepting updates for + * controls from the service. + * @property user the user for whose this service should be bound. + * @property componentName the name of the component for the service. + */ class ControlsProviderLifecycleManager( private val context: Context, private val executor: DelayableExecutor, @@ -51,7 +67,7 @@ class ControlsProviderLifecycleManager( val componentName: ComponentName ) : IBinder.DeathRecipient { - var lastLoadCallback: LoadCallback? = null + var lastLoadCallback: ControlsBindingController.LoadCallback? = null private set val token: IBinder = Binder() @GuardedBy("subscriptions") @@ -187,7 +203,7 @@ class ControlsProviderLifecycleManager( } } - private fun invokeOrQueue(f: () -> Unit, msg: Message) { + private inline fun invokeOrQueue(f: () -> Unit, msg: Message) { wrapper?.run { f() } ?: run { @@ -196,18 +212,37 @@ class ControlsProviderLifecycleManager( } } - fun maybeBindAndLoad(callback: LoadCallback) { + /** + * Request a call to [ControlsProviderService.loadAvailableControls]. + * + * If the service is not bound, the call will be queued and the service will be bound first. + * The service will be bound after the controls are returned or the call times out. + * + * @param callback a callback in which to return the result back. If the call times out + * [ControlsBindingController.LoadCallback.error] will be called instead. + */ + fun maybeBindAndLoad(callback: ControlsBindingController.LoadCallback) { unqueueMessage(Message.Unbind) lastLoadCallback = callback onLoadCanceller = executor.executeDelayed({ - // Didn't receive a response in time, log and send back empty list + // Didn't receive a response in time, log and send back error Log.d(TAG, "Timeout waiting onLoad for $componentName") - loadCallbackService.accept(token, emptyList()) + callback.error("Timeout waiting onLoad") + // Don't accept load callbacks after this + lastLoadCallback = null + unbindService() }, LOAD_TIMEOUT, TimeUnit.MILLISECONDS) invokeOrQueue(::load, Message.Load) } + /** + * Request a subscription to the [Publisher] returned by [ControlsProviderService.publisherFor] + * + * If the service is not bound, the call will be queued and the service will be bound first. + * + * @param controlIds a list of the ids of controls to send status back. + */ fun maybeBindAndSubscribe(controlIds: List) { invokeOrQueue({ subscribe(controlIds) }, Message.Subscribe(controlIds)) } @@ -222,6 +257,14 @@ class ControlsProviderLifecycleManager( } } + /** + * Request a call to [ControlsProviderService.performControlAction]. + * + * If the service is not bound, the call will be queued and the service will be bound first. + * + * @param controlId the id of the [Control] the action is performed on + * @param action the action performed + */ fun maybeBindAndSendAction(controlId: String, action: ControlAction) { invokeOrQueue({ action(controlId, action) }, Message.Action(controlId, action)) } @@ -236,6 +279,12 @@ class ControlsProviderLifecycleManager( } } + /** + * Starts the subscription to the [ControlsProviderService] and requests status of controls. + * + * @param subscription the subscriber to use to request controls + * @see maybeBindAndLoad + */ fun startSubscription(subscription: IControlsSubscription) { synchronized(subscriptions) { subscriptions.add(subscription) @@ -243,6 +292,9 @@ class ControlsProviderLifecycleManager( wrapper?.request(subscription, MAX_CONTROLS_REQUEST) } + /** + * Unsubscribe from this service, cancelling all status requests. + */ fun unsubscribe() { if (DEBUG) { Log.d(TAG, "unsubscribe $componentName") @@ -260,11 +312,17 @@ class ControlsProviderLifecycleManager( } } + /** + * Request bind to the service. + */ fun bindService() { unqueueMessage(Message.Unbind) bindService(true) } + /** + * Request unbind from the service. + */ fun unbindService() { lastLoadCallback = null onLoadCanceller?.run() @@ -281,6 +339,9 @@ class ControlsProviderLifecycleManager( }.toString() } + /** + * Messages for the internal queue. + */ sealed class Message { abstract val type: Int object Load : Message() { diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt index 5c812b1347e54..b90f892d5d266 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt @@ -25,12 +25,17 @@ import android.service.controls.IControlsSubscription import android.service.controls.actions.ControlActionWrapper import android.util.Log +/** + * Wrapper for the service calls. + * + * Calling all [IControlsProvider] methods through here will wrap them in a try/catch block. + */ class ServiceWrapper(val service: IControlsProvider) { companion object { private const val TAG = "ServiceWrapper" } - private fun callThroughService(block: () -> Unit): Boolean { + private inline fun callThroughService(block: () -> Unit): Boolean { try { block() return true 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 1e52371481880..9952b976a2542 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt @@ -21,6 +21,7 @@ import android.content.ComponentName import android.content.Intent import android.os.Bundle import android.view.LayoutInflater +import android.view.View import android.view.ViewStub import android.widget.Button import android.widget.TextView @@ -34,6 +35,7 @@ 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 java.util.function.Consumer import javax.inject.Inject class ControlsFavoritingActivity @Inject constructor( @@ -51,6 +53,7 @@ class ControlsFavoritingActivity @Inject constructor( private lateinit var adapterAll: ControlAdapter private lateinit var recyclerViewFavorites: RecyclerView private lateinit var adapterFavorites: ControlAdapter + private lateinit var errorText: TextView private var component: ComponentName? = null private var currentModel: FavoriteModel? = null @@ -96,6 +99,7 @@ class ControlsFavoritingActivity @Inject constructor( val app = intent.getCharSequenceExtra(EXTRA_APP) component = intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME) + errorText = requireViewById(R.id.error_message) setUpRecyclerViews() @@ -118,7 +122,10 @@ class ControlsFavoritingActivity @Inject constructor( } component?.let { - controller.loadForComponent(it) { allControls, favoriteKeys -> + controller.loadForComponent(it, Consumer { data -> + val allControls = data.allControls + val favoriteKeys = data.favoritesIds + val error = data.errorOnLoad executor.execute { val favoriteModel = FavoriteModel( allControls, @@ -128,8 +135,9 @@ class ControlsFavoritingActivity @Inject constructor( adapterAll.changeFavoritesModel(favoriteModel) adapterFavorites.changeFavoritesModel(favoriteModel) currentModel = favoriteModel + errorText.visibility = if (error) View.VISIBLE else View.GONE } - } + }) } currentUserTracker.startTracking() 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 34db684022fb0..d893caabdb36b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt @@ -21,11 +21,26 @@ import com.android.settingslib.widget.CandidateInfo import com.android.systemui.controls.UserAwareController import com.android.systemui.statusbar.policy.CallbackController +/** + * Controller for keeping track of services that can be bound given a particular [ServiceListing]. + */ interface ControlsListingController : CallbackController, UserAwareController { + /** + * @return the current list of services that satisfies the [ServiceListing]. + */ fun getCurrentServices(): List + + /** + * Get the app label for a given component. + * + * This call may do Binder calls (to [PackageManager]) + * + * @param name the component name to retrieve the label + * @return the label for the component + */ fun getAppLabel(name: ComponentName): CharSequence? = "" @FunctionalInterface 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 89c1636e953fa..40075c8413d2b 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 @@ -86,7 +86,11 @@ class ControlsBindingControllerTest : SysuiTestCase() { @Test fun testBindAndLoad() { - val callback: (List) -> Unit = {} + val callback = object : ControlsBindingController.LoadCallback { + override fun error(message: String) {} + + override fun accept(t: List) {} + } controller.bindAndLoad(TEST_COMPONENT_NAME_1, callback) assertEquals(1, providers.size) 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 751217f03fa3d..e5ec2dd887674 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 @@ -56,6 +56,7 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations import java.util.Optional +import java.util.function.Consumer @SmallTest @RunWith(AndroidTestingRunner::class) @@ -79,7 +80,8 @@ class ControlsControllerImplTest : SysuiTestCase() { @Captor private lateinit var controlInfoListCaptor: ArgumentCaptor> @Captor - private lateinit var controlLoadCallbackCaptor: ArgumentCaptor<(List) -> Unit> + private lateinit var controlLoadCallbackCaptor: + ArgumentCaptor @Captor private lateinit var broadcastReceiverCaptor: ArgumentCaptor @@ -226,13 +228,13 @@ class ControlsControllerImplTest : SysuiTestCase() { val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2) val control = builderFromInfo(newControlInfo).build() - controller.loadForComponent(TEST_COMPONENT) { _, _ -> Unit } + controller.loadForComponent(TEST_COMPONENT, Consumer {}) reset(persistenceWrapper) verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), capture(controlLoadCallbackCaptor)) - controlLoadCallbackCaptor.value.invoke(listOf(control)) + controlLoadCallbackCaptor.value.accept(listOf(control)) verify(persistenceWrapper).storeFavorites(listOf(newControlInfo)) } @@ -294,19 +296,22 @@ class ControlsControllerImplTest : SysuiTestCase() { var loaded = false val control = builderFromInfo(TEST_CONTROL_INFO).build() - controller.loadForComponent(TEST_COMPONENT) { controls, favorites -> + controller.loadForComponent(TEST_COMPONENT, Consumer { data -> + val controls = data.allControls + val favorites = data.favoritesIds loaded = true assertEquals(1, controls.size) val controlStatus = controls[0] assertEquals(ControlStatus(control, false), controlStatus) assertTrue(favorites.isEmpty()) - } + assertFalse(data.errorOnLoad) + }) verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), capture(controlLoadCallbackCaptor)) - controlLoadCallbackCaptor.value.invoke(listOf(control)) + controlLoadCallbackCaptor.value.accept(listOf(control)) assertTrue(loaded) } @@ -318,7 +323,9 @@ class ControlsControllerImplTest : SysuiTestCase() { val control2 = builderFromInfo(TEST_CONTROL_INFO_2).build() controller.changeFavoriteStatus(TEST_CONTROL_INFO, true) - controller.loadForComponent(TEST_COMPONENT) { controls, favorites -> + controller.loadForComponent(TEST_COMPONENT, Consumer { data -> + val controls = data.allControls + val favorites = data.favoritesIds loaded = true assertEquals(2, controls.size) val controlStatus = controls.first { it.control.controlId == TEST_CONTROL_ID } @@ -329,12 +336,13 @@ class ControlsControllerImplTest : SysuiTestCase() { assertEquals(1, favorites.size) assertEquals(TEST_CONTROL_ID, favorites[0]) - } + assertFalse(data.errorOnLoad) + }) verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), capture(controlLoadCallbackCaptor)) - controlLoadCallbackCaptor.value.invoke(listOf(control, control2)) + controlLoadCallbackCaptor.value.accept(listOf(control, control2)) assertTrue(loaded) } @@ -344,7 +352,9 @@ class ControlsControllerImplTest : SysuiTestCase() { var loaded = false controller.changeFavoriteStatus(TEST_CONTROL_INFO, true) - controller.loadForComponent(TEST_COMPONENT) { controls, favorites -> + controller.loadForComponent(TEST_COMPONENT, Consumer { data -> + val controls = data.allControls + val favorites = data.favoritesIds loaded = true assertEquals(1, controls.size) val controlStatus = controls[0] @@ -354,12 +364,41 @@ class ControlsControllerImplTest : SysuiTestCase() { assertEquals(1, favorites.size) assertEquals(TEST_CONTROL_ID, favorites[0]) - } + assertFalse(data.errorOnLoad) + }) verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), capture(controlLoadCallbackCaptor)) - controlLoadCallbackCaptor.value.invoke(emptyList()) + controlLoadCallbackCaptor.value.accept(emptyList()) + + assertTrue(loaded) + } + + @Test + fun testErrorOnLoad_notRemoved() { + var loaded = false + controller.changeFavoriteStatus(TEST_CONTROL_INFO, true) + + controller.loadForComponent(TEST_COMPONENT, Consumer { data -> + val controls = data.allControls + val favorites = data.favoritesIds + loaded = true + assertEquals(1, controls.size) + val controlStatus = controls[0] + assertEquals(TEST_CONTROL_ID, controlStatus.control.controlId) + assertTrue(controlStatus.favorite) + assertFalse(controlStatus.removed) + + assertEquals(1, favorites.size) + assertEquals(TEST_CONTROL_ID, favorites[0]) + assertTrue(data.errorOnLoad) + }) + + verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), + capture(controlLoadCallbackCaptor)) + + controlLoadCallbackCaptor.value.error("") assertTrue(loaded) } @@ -370,12 +409,12 @@ class ControlsControllerImplTest : SysuiTestCase() { val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2) val control = builderFromInfo(newControlInfo).build() - controller.loadForComponent(TEST_COMPONENT) { _, _ -> Unit } + controller.loadForComponent(TEST_COMPONENT, Consumer {}) verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), capture(controlLoadCallbackCaptor)) - controlLoadCallbackCaptor.value.invoke(listOf(control)) + controlLoadCallbackCaptor.value.accept(listOf(control)) val favorites = controller.getFavoriteControls() assertEquals(1, favorites.size) 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 40566dc39c9e5..ddd6b125dcfae 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 @@ -18,7 +18,6 @@ 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 import android.service.controls.IControlsProvider @@ -28,7 +27,6 @@ import android.service.controls.actions.ControlActionWrapper import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import org.junit.After @@ -41,6 +39,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.eq import org.mockito.Captor import org.mockito.Mock @@ -53,20 +52,22 @@ import org.mockito.MockitoAnnotations class ControlsProviderLifecycleManagerTest : SysuiTestCase() { @Mock - private lateinit var actionCallback: IControlsActionCallback.Stub + private lateinit var actionCallbackService: IControlsActionCallback.Stub @Mock - private lateinit var loadCallback: IControlsLoadCallback.Stub + private lateinit var loadCallbackService: IControlsLoadCallback.Stub @Mock - private lateinit var subscriber: IControlsSubscriber.Stub + private lateinit var subscriberService: IControlsSubscriber.Stub @Mock private lateinit var service: IControlsProvider.Stub + @Mock + private lateinit var loadCallback: ControlsBindingController.LoadCallback @Captor private lateinit var wrapperCaptor: ArgumentCaptor private val componentName = ComponentName("test.pkg", "test.cls") private lateinit var manager: ControlsProviderLifecycleManager - private lateinit var executor: DelayableExecutor + private lateinit var executor: FakeExecutor companion object { fun capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture() @@ -84,9 +85,9 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() { manager = ControlsProviderLifecycleManager( context, executor, - loadCallback, - actionCallback, - subscriber, + loadCallbackService, + actionCallbackService, + subscriberService, UserHandle.of(0), componentName ) @@ -112,31 +113,40 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() { @Test fun testMaybeBindAndLoad() { - val callback: (List) -> Unit = {} - manager.maybeBindAndLoad(callback) + manager.maybeBindAndLoad(loadCallback) - verify(service).load(loadCallback) + verify(service).load(loadCallbackService) assertTrue(mContext.isBound(componentName)) - assertEquals(callback, manager.lastLoadCallback) + assertEquals(loadCallback, manager.lastLoadCallback) } @Test fun testMaybeUnbind_bindingAndCallback() { - manager.maybeBindAndLoad {} + manager.maybeBindAndLoad(loadCallback) manager.unbindService() assertFalse(mContext.isBound(componentName)) assertNull(manager.lastLoadCallback) } + @Test + fun testMaybeBindAndLoad_timeout() { + manager.maybeBindAndLoad(loadCallback) + + executor.advanceClockToLast() + executor.runAllReady() + + verify(loadCallback).error(anyString()) + } + @Test fun testMaybeBindAndSubscribe() { val list = listOf("TEST_ID") manager.maybeBindAndSubscribe(list) assertTrue(mContext.isBound(componentName)) - verify(service).subscribe(list, subscriber) + verify(service).subscribe(list, subscriberService) } @Test @@ -147,7 +157,7 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() { assertTrue(mContext.isBound(componentName)) verify(service).action(eq(controlId), capture(wrapperCaptor), - eq(actionCallback)) + eq(actionCallbackService)) assertEquals(action, wrapperCaptor.getValue().getWrappedAction()) } } \ No newline at end of file