Merge "Added fallback for load failed"

This commit is contained in:
Matt Pietal
2020-02-12 23:41:22 +00:00
committed by Android (Google) Code Review
16 changed files with 550 additions and 86 deletions

View File

@@ -21,15 +21,32 @@
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/error_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/controls_management_list_margin"
android:text="@string/controls_favorite_load_error"
android:textAppearance="?android:attr/textAppearanceSmall"
android:visibility="gone"
android:gravity="center_horizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/text_favorites"
/>
<TextView
android:id="@+id/text_favorites"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="FAVORITES"
android:layout_marginTop="@dimen/controls_management_list_margin"
android:text="@string/controls_favorite_header_favorites"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAllCaps="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/divider1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/error_message"
/>
<View
@@ -60,8 +77,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/controls_management_list_margin"
android:text="ALL"
android:text="@string/controls_favorite_header_all"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAllCaps="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/divider2"
app:layout_constraintTop_toBottomOf="@id/listFavorites"

View File

@@ -2595,4 +2595,10 @@
<string name="controls_favorite_default_title">Controls</string>
<!-- Controls management controls screen subtitle [CHAR LIMIT=NONE] -->
<string name="controls_favorite_subtitle">Choose controls for quick access</string>
<!-- Controls management controls screen favorites header [CHAR LIMIT=50] -->
<string name="controls_favorite_header_favorites">Favorites</string>
<!-- Controls management controls screen all header [CHAR LIMIT=50] -->
<string name="controls_favorite_header_all">All</string>
<!-- Controls management controls screen error on load message [CHAR LIMIT=50] -->
<string name="controls_favorite_load_error">The list of all controls could not be loaded.</string>
</resources>

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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<Control>) -> 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<ComponentName>)
/**
* Send a subscribe message to retrieve status of a set of controls.
*
* The controls passed do not have to belong to a single [ControlsProviderService]. The
* corresponding service [ComponentName] is associated with each control.
*
* @param controls a list of controls with corresponding [ComponentName] to request status
* update
*/
fun subscribe(controls: List<ControlInfo>)
/**
* 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<List<Control>> {
/**
* Indicates an error loading.
*
* @message an error message.
*/
fun error(message: String)
}
}

View File

@@ -125,7 +125,10 @@ open class ControlsBindingControllerImpl @Inject constructor(
}
}
override fun bindAndLoad(component: ComponentName, callback: (List<Control>) -> 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()

View File

@@ -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<ControlInfo>
// 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<ControlStatus>, List<String>) -> Unit
dataCallback: Consumer<LoadData>
)
/**
* Request to subscribe for all favorite controls.
*
* @see [ControlsBindingController.subscribe]
*/
fun subscribeToFavorites()
fun changeFavoriteStatus(controlInfo: ControlInfo, state: Boolean)
fun replaceFavoritesForComponent(componentName: ComponentName, favorites: List<ControlInfo>)
fun getFavoritesForComponent(componentName: ComponentName): List<ControlInfo>
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<ControlInfo>
/**
* 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<ControlInfo>
/**
* 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<ControlInfo>)
/**
* 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<ControlStatus>
/**
* Ordered list of ids of favorite controls.
*/
val favoritesIds: List<String>
/**
* 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<ControlStatus>,
favorites: List<String>,
error: Boolean = false
): ControlsController.LoadData {
return object : ControlsController.LoadData {
override val allControls = allControls
override val favoritesIds = favorites
override val errorOnLoad = error
}
}

View File

@@ -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<ControlStatus>, List<String>) -> Unit
dataCallback: Consumer<ControlsController.LoadData>
) {
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<String> =
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<Control>) {
val loadData = synchronized(currentFavorites) {
val favoritesForComponentKeys: List<String> =
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")

View File

@@ -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<ControlInfo>) {
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<ControlInfo> {
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 {

View File

@@ -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<Control>) -> 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<String>) {
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() {

View File

@@ -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

View File

@@ -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<ComponentName>(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()

View File

@@ -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<ControlsListingController.ControlsListingCallback>,
UserAwareController {
/**
* @return the current list of services that satisfies the [ServiceListing].
*/
fun getCurrentServices(): List<CandidateInfo>
/**
* 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

View File

@@ -86,7 +86,11 @@ class ControlsBindingControllerTest : SysuiTestCase() {
@Test
fun testBindAndLoad() {
val callback: (List<Control>) -> Unit = {}
val callback = object : ControlsBindingController.LoadCallback {
override fun error(message: String) {}
override fun accept(t: List<Control>) {}
}
controller.bindAndLoad(TEST_COMPONENT_NAME_1, callback)
assertEquals(1, providers.size)

View File

@@ -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<List<ControlInfo>>
@Captor
private lateinit var controlLoadCallbackCaptor: ArgumentCaptor<(List<Control>) -> Unit>
private lateinit var controlLoadCallbackCaptor:
ArgumentCaptor<ControlsBindingController.LoadCallback>
@Captor
private lateinit var broadcastReceiverCaptor: ArgumentCaptor<BroadcastReceiver>
@@ -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)

View File

@@ -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<ControlActionWrapper>
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 <T> capture(argumentCaptor: ArgumentCaptor<T>): 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<Control>) -> 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())
}
}