From 9c459e58b62a6cabe344a352d8c02e380d66a0ca Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Wed, 12 Feb 2020 09:08:15 -0500 Subject: [PATCH] Added fallback for load failed If the load call times out or fails in some other way, the management acitivity will show that and just show all favorites (without the removed tag). Also, added many docs Test: atest Test: manual Fixes: 149138396 Change-Id: I58e391b65df1e40989101f227c46fd47e3fb419a --- .../layout/controls_management_favorites.xml | 24 ++- packages/SystemUI/res/values/strings.xml | 6 + .../systemui/controls/UserAwareController.kt | 5 +- .../controls/controller/ControlInfo.kt | 23 +++ .../controller/ControlsBindingController.kt | 61 +++++- .../ControlsBindingControllerImpl.kt | 7 +- .../controls/controller/ControlsController.kt | 175 +++++++++++++++++- .../controller/ControlsControllerImpl.kt | 81 +++++--- .../ControlsFavoritePersistenceWrapper.kt | 30 ++- .../ControlsProviderLifecycleManager.kt | 75 +++++++- .../controls/controller/ServiceWrapper.kt | 7 +- .../management/ControlsFavoritingActivity.kt | 12 +- .../management/ControlsListingController.kt | 15 ++ .../ControlsBindingControllerImplTest.kt | 6 +- .../controller/ControlsControllerImplTest.kt | 67 +++++-- .../ControlsProviderLifecycleManagerTest.kt | 42 +++-- 16 files changed, 550 insertions(+), 86 deletions(-) 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