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