Merge "Controls UI - Suggested controls" into rvc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
653604d01b
@@ -40,14 +40,20 @@
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.ControlSetup.Title"
|
||||
android:id="@+id/controls_title"
|
||||
android:text="@string/quick_controls_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:layout_gravity="center"
|
||||
android:textSize="25sp"
|
||||
android:textColor="@*android:color/foreground_material_dark"
|
||||
android:fontFamily="@*android:string/config_headlineFontFamily" />
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.ControlSetup.Subtitle"
|
||||
android:id="@+id/controls_subtitle"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
</LinearLayout>
|
||||
</merge>
|
||||
|
||||
@@ -532,4 +532,8 @@
|
||||
<!-- Respect drawable/rounded.xml intrinsic size for multiple radius corner path customization -->
|
||||
<bool name="config_roundedCornerMultipleRadius">false</bool>
|
||||
|
||||
<!-- Controls can query a preferred application for limited number of suggested controls.
|
||||
This config value should contain the package name of that preferred application.
|
||||
-->
|
||||
<string translatable="false" name="config_controlsPreferredPackage"></string>
|
||||
</resources>
|
||||
|
||||
@@ -2657,4 +2657,8 @@
|
||||
|
||||
<!-- Tooltip to show in management screen when there are multiple structures [CHAR_LIMIT=50] -->
|
||||
<string name="controls_structure_tooltip">Swipe to see other structures</string>
|
||||
|
||||
<!-- Message to tell the user to wait while systemui attempts to load a set of
|
||||
recommended controls [CHAR_LIMIT=30] -->
|
||||
<string name="controls_seeding_in_progress">Loading recommendations</string>
|
||||
</resources>
|
||||
|
||||
@@ -699,6 +699,20 @@
|
||||
<item name="*android:colorPopupBackground">@color/control_list_popup_background</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.ControlSetup">
|
||||
<item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
|
||||
<item name="android:textColor">@color/control_primary_text</item>
|
||||
<item name="android:singleLine">true</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.ControlSetup.Title">
|
||||
<item name="android:textSize">25sp</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.ControlSetup.Subtitle">
|
||||
<item name="android:textSize">16sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.ControlsRequestDialog" parent="@style/Theme.SystemUI.MediaProjectionAlertDialog"/>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -42,6 +42,14 @@ interface ControlsBindingController : UserAwareController {
|
||||
*/
|
||||
fun bindAndLoad(component: ComponentName, callback: LoadCallback): Runnable
|
||||
|
||||
/**
|
||||
* Request bind to a service and load a limited number of suggested controls.
|
||||
*
|
||||
* @param component The [ComponentName] of the service to bind
|
||||
* @param callback a callback to return the loaded controls to (or an error).
|
||||
*/
|
||||
fun bindAndLoadSuggested(component: ComponentName, callback: LoadCallback)
|
||||
|
||||
/**
|
||||
* Request to bind to the given service.
|
||||
*
|
||||
|
||||
@@ -44,6 +44,8 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ControlsBindingControllerImpl"
|
||||
private const val MAX_CONTROLS_REQUEST = 100000L
|
||||
private const val SUGGESTED_CONTROLS_REQUEST = 4L
|
||||
}
|
||||
|
||||
private var currentUser = UserHandle.of(ActivityManager.getCurrentUser())
|
||||
@@ -97,24 +99,37 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
component: ComponentName,
|
||||
callback: ControlsBindingController.LoadCallback
|
||||
): Runnable {
|
||||
val subscriber = LoadSubscriber(callback)
|
||||
val subscriber = LoadSubscriber(callback, MAX_CONTROLS_REQUEST)
|
||||
retrieveLifecycleManager(component).maybeBindAndLoad(subscriber)
|
||||
return subscriber.loadCancel()
|
||||
}
|
||||
|
||||
override fun bindAndLoadSuggested(
|
||||
component: ComponentName,
|
||||
callback: ControlsBindingController.LoadCallback
|
||||
) {
|
||||
val subscriber = LoadSubscriber(callback, SUGGESTED_CONTROLS_REQUEST)
|
||||
retrieveLifecycleManager(component).maybeBindAndLoadSuggested(subscriber)
|
||||
}
|
||||
|
||||
override fun subscribe(structureInfo: StructureInfo) {
|
||||
// make sure this has happened. only allow one active subscription
|
||||
unsubscribe()
|
||||
|
||||
statefulControlSubscriber = null
|
||||
val provider = retrieveLifecycleManager(structureInfo.componentName)
|
||||
val scs = StatefulControlSubscriber(lazyController.get(), provider, backgroundExecutor)
|
||||
val scs = StatefulControlSubscriber(
|
||||
lazyController.get(),
|
||||
provider,
|
||||
backgroundExecutor,
|
||||
MAX_CONTROLS_REQUEST
|
||||
)
|
||||
statefulControlSubscriber = scs
|
||||
provider.maybeBindAndSubscribe(structureInfo.controls.map { it.controlId }, scs)
|
||||
}
|
||||
|
||||
override fun unsubscribe() {
|
||||
statefulControlSubscriber?.cancel()
|
||||
statefulControlSubscriber = null
|
||||
}
|
||||
|
||||
override fun action(
|
||||
@@ -201,10 +216,11 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
|
||||
private inner class OnSubscribeRunnable(
|
||||
token: IBinder,
|
||||
val subscription: IControlsSubscription
|
||||
val subscription: IControlsSubscription,
|
||||
val requestLimit: Long
|
||||
) : CallbackRunnable(token) {
|
||||
override fun doRun() {
|
||||
provider?.startSubscription(subscription)
|
||||
provider?.startSubscription(subscription, requestLimit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +250,8 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
}
|
||||
|
||||
private inner class LoadSubscriber(
|
||||
val callback: ControlsBindingController.LoadCallback
|
||||
val callback: ControlsBindingController.LoadCallback,
|
||||
val requestLimit: Long
|
||||
) : IControlsSubscriber.Stub() {
|
||||
val loadedControls = ArrayList<Control>()
|
||||
var hasError = false
|
||||
@@ -246,7 +263,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
|
||||
override fun onSubscribe(token: IBinder, subs: IControlsSubscription) {
|
||||
_loadCancelInternal = subs::cancel
|
||||
backgroundExecutor.execute(OnSubscribeRunnable(token, subs))
|
||||
backgroundExecutor.execute(OnSubscribeRunnable(token, subs, requestLimit))
|
||||
}
|
||||
|
||||
override fun onNext(token: IBinder, c: Control) {
|
||||
|
||||
@@ -113,6 +113,25 @@ interface ControlsController : UserAwareController {
|
||||
|
||||
// FAVORITE MANAGEMENT
|
||||
|
||||
/**
|
||||
* Send a request to seed favorites into the persisted XML file
|
||||
*
|
||||
* @param componentName the component to seed controls from
|
||||
* @param callback true if the favorites were persisted
|
||||
*/
|
||||
fun seedFavoritesForComponent(
|
||||
componentName: ComponentName,
|
||||
callback: Consumer<Boolean>
|
||||
)
|
||||
|
||||
/**
|
||||
* Callback to be informed when the seeding process has finished
|
||||
*
|
||||
* @param callback consumer accepts true if successful
|
||||
* @return true if seeding is in progress and the callback was added
|
||||
*/
|
||||
fun addSeedingFavoritesCallback(callback: Consumer<Boolean>): Boolean
|
||||
|
||||
/**
|
||||
* Get all the favorites.
|
||||
*
|
||||
|
||||
@@ -31,6 +31,7 @@ import android.os.UserHandle
|
||||
import android.provider.Settings
|
||||
import android.service.controls.Control
|
||||
import android.service.controls.actions.ControlAction
|
||||
import android.util.ArrayMap
|
||||
import android.util.Log
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import com.android.systemui.Dumpable
|
||||
@@ -74,6 +75,9 @@ class ControlsControllerImpl @Inject constructor (
|
||||
|
||||
private var loadCanceller: Runnable? = null
|
||||
|
||||
private var seedingInProgress = false
|
||||
private val seedingCallbacks = mutableListOf<Consumer<Boolean>>()
|
||||
|
||||
private var currentUser = UserHandle.of(ActivityManager.getCurrentUser())
|
||||
override val currentUserId
|
||||
get() = currentUser.identifier
|
||||
@@ -280,6 +284,84 @@ class ControlsControllerImpl @Inject constructor (
|
||||
)
|
||||
}
|
||||
|
||||
override fun addSeedingFavoritesCallback(callback: Consumer<Boolean>): Boolean {
|
||||
if (!seedingInProgress) return false
|
||||
executor.execute {
|
||||
// status may have changed by this point, so check again and inform the
|
||||
// caller if necessary
|
||||
if (seedingInProgress) seedingCallbacks.add(callback)
|
||||
else callback.accept(false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun seedFavoritesForComponent(
|
||||
componentName: ComponentName,
|
||||
callback: Consumer<Boolean>
|
||||
) {
|
||||
Log.i(TAG, "Beginning request to seed favorites for: $componentName")
|
||||
if (!confirmAvailability()) {
|
||||
if (userChanging) {
|
||||
// Try again later, userChanging should not last forever. If so, we have bigger
|
||||
// problems. This will return a runnable that allows to cancel the delayed version,
|
||||
// it will not be able to cancel the load if
|
||||
executor.executeDelayed(
|
||||
{ seedFavoritesForComponent(componentName, callback) },
|
||||
USER_CHANGE_RETRY_DELAY,
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
} else {
|
||||
callback.accept(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
seedingInProgress = true
|
||||
bindingController.bindAndLoadSuggested(
|
||||
componentName,
|
||||
object : ControlsBindingController.LoadCallback {
|
||||
override fun accept(controls: List<Control>) {
|
||||
executor.execute {
|
||||
val structureToControls =
|
||||
ArrayMap<CharSequence, MutableList<ControlInfo>>()
|
||||
|
||||
controls.forEach {
|
||||
val structure = it.structure ?: ""
|
||||
val list = structureToControls.get(structure)
|
||||
?: mutableListOf<ControlInfo>()
|
||||
list.add(ControlInfo(it.controlId, it.title, it.deviceType))
|
||||
structureToControls.put(structure, list)
|
||||
}
|
||||
|
||||
structureToControls.forEach {
|
||||
(s, cs) -> Favorites.replaceControls(
|
||||
StructureInfo(componentName, s, cs))
|
||||
}
|
||||
|
||||
persistenceWrapper.storeFavorites(Favorites.getAllStructures())
|
||||
callback.accept(true)
|
||||
endSeedingCall(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun error(message: String) {
|
||||
Log.e(TAG, "Unable to seed favorites: $message")
|
||||
executor.execute {
|
||||
callback.accept(false)
|
||||
endSeedingCall(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun endSeedingCall(state: Boolean) {
|
||||
seedingInProgress = false
|
||||
seedingCallbacks.forEach {
|
||||
it.accept(state)
|
||||
}
|
||||
seedingCallbacks.clear()
|
||||
}
|
||||
|
||||
override fun cancelLoad() {
|
||||
loadCanceller?.let {
|
||||
executor.execute(it)
|
||||
|
||||
@@ -66,22 +66,17 @@ class ControlsProviderLifecycleManager(
|
||||
@GuardedBy("subscriptions")
|
||||
private val subscriptions = mutableListOf<IControlsSubscription>()
|
||||
private var requiresBound = false
|
||||
@GuardedBy("queuedMessages")
|
||||
private val queuedMessages: MutableSet<Message> = ArraySet()
|
||||
@GuardedBy("queuedServiceMethods")
|
||||
private val queuedServiceMethods: MutableSet<ServiceMethod> = ArraySet()
|
||||
private var wrapper: ServiceWrapper? = null
|
||||
private var bindTryCount = 0
|
||||
private val TAG = javaClass.simpleName
|
||||
private var onLoadCanceller: Runnable? = null
|
||||
|
||||
companion object {
|
||||
private const val MSG_LOAD = 0
|
||||
private const val MSG_SUBSCRIBE = 1
|
||||
private const val MSG_ACTION = 2
|
||||
private const val MSG_UNBIND = 3
|
||||
private const val BIND_RETRY_DELAY = 1000L // ms
|
||||
private const val LOAD_TIMEOUT_SECONDS = 30L // seconds
|
||||
private const val MAX_BIND_RETRIES = 5
|
||||
private const val MAX_CONTROLS_REQUEST = 100000L
|
||||
private const val DEBUG = true
|
||||
private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or
|
||||
Context.BIND_WAIVE_PRIORITY
|
||||
@@ -130,7 +125,7 @@ class ControlsProviderLifecycleManager(
|
||||
try {
|
||||
service.linkToDeath(this@ControlsProviderLifecycleManager, 0)
|
||||
} catch (_: RemoteException) {}
|
||||
handlePendingMessages()
|
||||
handlePendingServiceMethods()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
@@ -140,29 +135,14 @@ class ControlsProviderLifecycleManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePendingMessages() {
|
||||
val queue = synchronized(queuedMessages) {
|
||||
ArraySet(queuedMessages).also {
|
||||
queuedMessages.clear()
|
||||
private fun handlePendingServiceMethods() {
|
||||
val queue = synchronized(queuedServiceMethods) {
|
||||
ArraySet(queuedServiceMethods).also {
|
||||
queuedServiceMethods.clear()
|
||||
}
|
||||
}
|
||||
if (Message.Unbind in queue) {
|
||||
bindService(false)
|
||||
return
|
||||
}
|
||||
|
||||
queue.filter { it is Message.Load }.forEach {
|
||||
val msg = it as Message.Load
|
||||
load(msg.subscriber)
|
||||
}
|
||||
|
||||
queue.filter { it is Message.Subscribe }.forEach {
|
||||
val msg = it as Message.Subscribe
|
||||
subscribe(msg.list, msg.subscriber)
|
||||
}
|
||||
queue.filter { it is Message.Action }.forEach {
|
||||
val msg = it as Message.Action
|
||||
action(msg.id, msg.action)
|
||||
queue.forEach {
|
||||
it.run()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,33 +157,17 @@ class ControlsProviderLifecycleManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun queueMessage(message: Message) {
|
||||
synchronized(queuedMessages) {
|
||||
queuedMessages.add(message)
|
||||
private fun queueServiceMethod(sm: ServiceMethod) {
|
||||
synchronized(queuedServiceMethods) {
|
||||
queuedServiceMethods.add(sm)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unqueueMessageType(type: Int) {
|
||||
synchronized(queuedMessages) {
|
||||
queuedMessages.removeIf { it.type == type }
|
||||
}
|
||||
}
|
||||
|
||||
private fun load(subscriber: IControlsSubscriber.Stub) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "load $componentName")
|
||||
}
|
||||
if (!(wrapper?.load(subscriber) ?: false)) {
|
||||
queueMessage(Message.Load(subscriber))
|
||||
binderDied()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun invokeOrQueue(f: () -> Unit, msg: Message) {
|
||||
private fun invokeOrQueue(sm: ServiceMethod) {
|
||||
wrapper?.run {
|
||||
f()
|
||||
sm.run()
|
||||
} ?: run {
|
||||
queueMessage(msg)
|
||||
queueServiceMethod(sm)
|
||||
bindService(true)
|
||||
}
|
||||
}
|
||||
@@ -217,7 +181,6 @@ class ControlsProviderLifecycleManager(
|
||||
* @param subscriber the subscriber that manages coordination for loading controls
|
||||
*/
|
||||
fun maybeBindAndLoad(subscriber: IControlsSubscriber.Stub) {
|
||||
unqueueMessageType(MSG_UNBIND)
|
||||
onLoadCanceller = executor.executeDelayed({
|
||||
// Didn't receive a response in time, log and send back error
|
||||
Log.d(TAG, "Timeout waiting onLoad for $componentName")
|
||||
@@ -225,7 +188,26 @@ class ControlsProviderLifecycleManager(
|
||||
unbindService()
|
||||
}, LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
|
||||
invokeOrQueue({ load(subscriber) }, Message.Load(subscriber))
|
||||
invokeOrQueue(Load(subscriber))
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a call to [IControlsProvider.loadSuggested].
|
||||
*
|
||||
* If the service is not bound, the call will be queued and the service will be bound first.
|
||||
* The service will be unbound after the controls are returned or the call times out.
|
||||
*
|
||||
* @param subscriber the subscriber that manages coordination for loading controls
|
||||
*/
|
||||
fun maybeBindAndLoadSuggested(subscriber: IControlsSubscriber.Stub) {
|
||||
onLoadCanceller = executor.executeDelayed({
|
||||
// Didn't receive a response in time, log and send back error
|
||||
Log.d(TAG, "Timeout waiting onLoadSuggested for $componentName")
|
||||
subscriber.onError(token, "Timeout waiting onLoadSuggested")
|
||||
unbindService()
|
||||
}, LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
|
||||
invokeOrQueue(Suggest(subscriber))
|
||||
}
|
||||
|
||||
fun cancelLoadTimeout() {
|
||||
@@ -240,23 +222,8 @@ class ControlsProviderLifecycleManager(
|
||||
*
|
||||
* @param controlIds a list of the ids of controls to send status back.
|
||||
*/
|
||||
fun maybeBindAndSubscribe(controlIds: List<String>, subscriber: IControlsSubscriber) {
|
||||
invokeOrQueue(
|
||||
{ subscribe(controlIds, subscriber) },
|
||||
Message.Subscribe(controlIds, subscriber)
|
||||
)
|
||||
}
|
||||
|
||||
private fun subscribe(controlIds: List<String>, subscriber: IControlsSubscriber) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "subscribe $componentName - $controlIds")
|
||||
}
|
||||
|
||||
if (!(wrapper?.subscribe(controlIds, subscriber) ?: false)) {
|
||||
queueMessage(Message.Subscribe(controlIds, subscriber))
|
||||
binderDied()
|
||||
}
|
||||
}
|
||||
fun maybeBindAndSubscribe(controlIds: List<String>, subscriber: IControlsSubscriber) =
|
||||
invokeOrQueue(Subscribe(controlIds, subscriber))
|
||||
|
||||
/**
|
||||
* Request a call to [ControlsProviderService.performControlAction].
|
||||
@@ -266,19 +233,8 @@ class ControlsProviderLifecycleManager(
|
||||
* @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))
|
||||
}
|
||||
|
||||
private fun action(controlId: String, action: ControlAction) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onAction $componentName - $controlId")
|
||||
}
|
||||
if (!(wrapper?.action(controlId, action, actionCallbackService) ?: false)) {
|
||||
queueMessage(Message.Action(controlId, action))
|
||||
binderDied()
|
||||
}
|
||||
}
|
||||
fun maybeBindAndSendAction(controlId: String, action: ControlAction) =
|
||||
invokeOrQueue(Action(controlId, action))
|
||||
|
||||
/**
|
||||
* Starts the subscription to the [ControlsProviderService] and requests status of controls.
|
||||
@@ -286,14 +242,14 @@ class ControlsProviderLifecycleManager(
|
||||
* @param subscription the subscription to use to request controls
|
||||
* @see maybeBindAndLoad
|
||||
*/
|
||||
fun startSubscription(subscription: IControlsSubscription) {
|
||||
fun startSubscription(subscription: IControlsSubscription, requestLimit: Long) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "startSubscription: $subscription")
|
||||
}
|
||||
synchronized(subscriptions) {
|
||||
subscriptions.add(subscription)
|
||||
}
|
||||
wrapper?.request(subscription, MAX_CONTROLS_REQUEST)
|
||||
wrapper?.request(subscription, requestLimit)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,7 +272,6 @@ class ControlsProviderLifecycleManager(
|
||||
* Request bind to the service.
|
||||
*/
|
||||
fun bindService() {
|
||||
unqueueMessageType(MSG_UNBIND)
|
||||
bindService(true)
|
||||
}
|
||||
|
||||
@@ -350,21 +305,55 @@ class ControlsProviderLifecycleManager(
|
||||
}
|
||||
|
||||
/**
|
||||
* Messages for the internal queue.
|
||||
* Service methods that can be queued or invoked, and are retryable for failure scenarios
|
||||
*/
|
||||
sealed class Message {
|
||||
abstract val type: Int
|
||||
class Load(val subscriber: IControlsSubscriber.Stub) : Message() {
|
||||
override val type = MSG_LOAD
|
||||
abstract inner class ServiceMethod {
|
||||
fun run() {
|
||||
if (!callWrapper()) {
|
||||
queueServiceMethod(this)
|
||||
binderDied()
|
||||
}
|
||||
}
|
||||
object Unbind : Message() {
|
||||
override val type = MSG_UNBIND
|
||||
|
||||
internal abstract fun callWrapper(): Boolean
|
||||
}
|
||||
|
||||
inner class Load(val subscriber: IControlsSubscriber.Stub) : ServiceMethod() {
|
||||
override fun callWrapper(): Boolean {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "load $componentName")
|
||||
}
|
||||
return wrapper?.load(subscriber) ?: false
|
||||
}
|
||||
class Subscribe(val list: List<String>, val subscriber: IControlsSubscriber) : Message() {
|
||||
override val type = MSG_SUBSCRIBE
|
||||
}
|
||||
|
||||
inner class Suggest(val subscriber: IControlsSubscriber.Stub) : ServiceMethod() {
|
||||
override fun callWrapper(): Boolean {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "suggest $componentName")
|
||||
}
|
||||
return wrapper?.loadSuggested(subscriber) ?: false
|
||||
}
|
||||
class Action(val id: String, val action: ControlAction) : Message() {
|
||||
override val type = MSG_ACTION
|
||||
}
|
||||
inner class Subscribe(
|
||||
val list: List<String>,
|
||||
val subscriber: IControlsSubscriber
|
||||
) : ServiceMethod() {
|
||||
override fun callWrapper(): Boolean {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "subscribe $componentName - $list")
|
||||
}
|
||||
|
||||
return wrapper?.subscribe(list, subscriber) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
inner class Action(val id: String, val action: ControlAction) : ServiceMethod() {
|
||||
override fun callWrapper(): Boolean {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onAction $componentName - $id")
|
||||
}
|
||||
return wrapper?.action(id, action, actionCallbackService) ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,12 @@ class ServiceWrapper(val service: IControlsProvider) {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSuggested(subscriber: IControlsSubscriber): Boolean {
|
||||
return callThroughService {
|
||||
service.loadSuggested(subscriber)
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribe(controlIds: List<String>, subscriber: IControlsSubscriber): Boolean {
|
||||
return callThroughService {
|
||||
service.subscribe(controlIds, subscriber)
|
||||
|
||||
@@ -31,7 +31,8 @@ import com.android.systemui.util.concurrency.DelayableExecutor
|
||||
class StatefulControlSubscriber(
|
||||
private val controller: ControlsController,
|
||||
private val provider: ControlsProviderLifecycleManager,
|
||||
private val bgExecutor: DelayableExecutor
|
||||
private val bgExecutor: DelayableExecutor,
|
||||
private val requestLimit: Long
|
||||
) : IControlsSubscriber.Stub() {
|
||||
private var subscriptionOpen = false
|
||||
private var subscription: IControlsSubscription? = null
|
||||
@@ -50,7 +51,7 @@ class StatefulControlSubscriber(
|
||||
run(token) {
|
||||
subscriptionOpen = true
|
||||
subscription = subs
|
||||
provider.startSubscription(subs)
|
||||
provider.startSubscription(subs, requestLimit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class ControlsRequestDialog @Inject constructor(
|
||||
private lateinit var control: Control
|
||||
private var dialog: Dialog? = null
|
||||
private val callback = object : ControlsListingController.ControlsListingCallback {
|
||||
override fun onServicesUpdated(candidates: List<ControlsServiceInfo>) {}
|
||||
override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {}
|
||||
}
|
||||
|
||||
private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
|
||||
|
||||
@@ -52,6 +52,7 @@ import com.android.systemui.R
|
||||
import dagger.Lazy
|
||||
|
||||
import java.text.Collator
|
||||
import java.util.function.Consumer
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -89,6 +90,7 @@ class ControlsUiControllerImpl @Inject constructor (
|
||||
private var popup: ListPopupWindow? = null
|
||||
private var activeDialog: Dialog? = null
|
||||
private val addControlsItem: SelectionItem
|
||||
private var hidden = true
|
||||
|
||||
init {
|
||||
val addDrawable = context.getDrawable(R.drawable.ic_add).apply {
|
||||
@@ -134,11 +136,15 @@ class ControlsUiControllerImpl @Inject constructor (
|
||||
override fun show(parent: ViewGroup) {
|
||||
Log.d(ControlsUiController.TAG, "show()")
|
||||
this.parent = parent
|
||||
hidden = false
|
||||
|
||||
allStructures = controlsController.get().getFavorites()
|
||||
selectedStructure = loadPreference(allStructures)
|
||||
|
||||
if (selectedStructure.controls.isEmpty() && allStructures.size <= 1) {
|
||||
val cb = Consumer<Boolean> { _ -> reload(parent) }
|
||||
if (controlsController.get().addSeedingFavoritesCallback(cb)) {
|
||||
listingCallback = createCallback(::showSeedingView)
|
||||
} else if (selectedStructure.controls.isEmpty() && allStructures.size <= 1) {
|
||||
// only show initial view if there are really no favorites across any structure
|
||||
listingCallback = createCallback(::showInitialSetupView)
|
||||
} else {
|
||||
@@ -154,6 +160,20 @@ class ControlsUiControllerImpl @Inject constructor (
|
||||
controlsListingController.get().addCallback(listingCallback)
|
||||
}
|
||||
|
||||
private fun reload(parent: ViewGroup) {
|
||||
if (hidden) return
|
||||
show(parent)
|
||||
}
|
||||
|
||||
private fun showSeedingView(items: List<SelectionItem>) {
|
||||
parent.removeAllViews()
|
||||
|
||||
val inflater = LayoutInflater.from(context)
|
||||
inflater.inflate(R.layout.controls_no_favorites, parent, true)
|
||||
val subtitle = parent.requireViewById<TextView>(R.id.controls_subtitle)
|
||||
subtitle.setVisibility(View.VISIBLE)
|
||||
}
|
||||
|
||||
private fun showInitialSetupView(items: List<SelectionItem>) {
|
||||
parent.removeAllViews()
|
||||
|
||||
@@ -320,13 +340,14 @@ class ControlsUiControllerImpl @Inject constructor (
|
||||
selectedStructure = newSelection
|
||||
updatePreferences(selectedStructure)
|
||||
controlsListingController.get().removeCallback(listingCallback)
|
||||
show(parent)
|
||||
reload(parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun hide() {
|
||||
Log.d(ControlsUiController.TAG, "hide()")
|
||||
hidden = true
|
||||
popup?.dismiss()
|
||||
activeDialog?.dismiss()
|
||||
|
||||
|
||||
@@ -31,11 +31,13 @@ import android.app.WallpaperManager;
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.app.trust.TrustManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.content.res.Resources;
|
||||
import android.database.ContentObserver;
|
||||
@@ -92,6 +94,8 @@ import com.android.systemui.MultiListLayout;
|
||||
import com.android.systemui.MultiListLayout.MultiListAdapter;
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||
import com.android.systemui.colorextraction.SysuiColorExtractor;
|
||||
import com.android.systemui.controls.ControlsServiceInfo;
|
||||
import com.android.systemui.controls.controller.ControlsController;
|
||||
import com.android.systemui.controls.management.ControlsListingController;
|
||||
import com.android.systemui.controls.ui.ControlsUiController;
|
||||
import com.android.systemui.dagger.qualifiers.Background;
|
||||
@@ -148,6 +152,9 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener,
|
||||
private static final String GLOBAL_ACTION_KEY_EMERGENCY = "emergency";
|
||||
private static final String GLOBAL_ACTION_KEY_SCREENSHOT = "screenshot";
|
||||
|
||||
private static final String PREFS_CONTROLS_SEEDING_COMPLETED = "ControlsSeedingCompleted";
|
||||
private static final String PREFS_CONTROLS_FILE = "controls_prefs";
|
||||
|
||||
private final Context mContext;
|
||||
private final GlobalActionsManager mWindowManagerFuncs;
|
||||
private final AudioManager mAudioManager;
|
||||
@@ -215,7 +222,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener,
|
||||
NotificationShadeWindowController notificationShadeWindowController,
|
||||
ControlsUiController controlsUiController, IWindowManager iWindowManager,
|
||||
@Background Executor backgroundExecutor,
|
||||
ControlsListingController controlsListingController) {
|
||||
ControlsListingController controlsListingController,
|
||||
ControlsController controlsController) {
|
||||
mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme);
|
||||
mWindowManagerFuncs = windowManagerFuncs;
|
||||
mAudioManager = audioManager;
|
||||
@@ -279,9 +287,46 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener,
|
||||
}
|
||||
});
|
||||
|
||||
mControlsListingController.addCallback(list -> mAnyControlsProviders = !list.isEmpty());
|
||||
String preferredControlsPackage = mContext.getResources()
|
||||
.getString(com.android.systemui.R.string.config_controlsPreferredPackage);
|
||||
mControlsListingController.addCallback(list -> {
|
||||
mAnyControlsProviders = !list.isEmpty();
|
||||
|
||||
/*
|
||||
* See if any service providers match the preferred component. If they do,
|
||||
* and there are no current favorites, and we haven't successfully loaded favorites to
|
||||
* date, query the preferred component for a limited number of suggested controls.
|
||||
*/
|
||||
ComponentName preferredComponent = null;
|
||||
for (ControlsServiceInfo info : list) {
|
||||
if (info.componentName.getPackageName().equals(preferredControlsPackage)) {
|
||||
preferredComponent = info.componentName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (preferredComponent == null) return;
|
||||
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_CONTROLS_FILE,
|
||||
Context.MODE_PRIVATE);
|
||||
boolean isSeeded = prefs.getBoolean(PREFS_CONTROLS_SEEDING_COMPLETED, false);
|
||||
boolean hasFavorites = controlsController.getFavorites().size() > 0;
|
||||
if (!isSeeded && !hasFavorites) {
|
||||
controlsController.seedFavoritesForComponent(
|
||||
preferredComponent,
|
||||
(accepted) -> {
|
||||
Log.i(TAG, "Controls seeded: " + accepted);
|
||||
prefs.edit().putBoolean(PREFS_CONTROLS_SEEDING_COMPLETED,
|
||||
accepted).apply();
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Show the global actions dialog (creating if necessary)
|
||||
*
|
||||
|
||||
@@ -206,6 +206,56 @@ class ControlsBindingControllerImplTest : SysuiTestCase() {
|
||||
verify(subscription, never()).cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBindAndLoadSuggested() {
|
||||
val callback = object : ControlsBindingController.LoadCallback {
|
||||
override fun error(message: String) {}
|
||||
|
||||
override fun accept(t: List<Control>) {}
|
||||
}
|
||||
controller.bindAndLoadSuggested(TEST_COMPONENT_NAME_1, callback)
|
||||
|
||||
verify(providers[0]).maybeBindAndLoadSuggested(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLoadSuggested_onCompleteRemovesTimeout() {
|
||||
val callback = object : ControlsBindingController.LoadCallback {
|
||||
override fun error(message: String) {}
|
||||
|
||||
override fun accept(t: List<Control>) {}
|
||||
}
|
||||
val subscription = mock(IControlsSubscription::class.java)
|
||||
|
||||
controller.bindAndLoadSuggested(TEST_COMPONENT_NAME_1, callback)
|
||||
|
||||
verify(providers[0]).maybeBindAndLoadSuggested(capture(subscriberCaptor))
|
||||
val b = Binder()
|
||||
subscriberCaptor.value.onSubscribe(b, subscription)
|
||||
|
||||
subscriberCaptor.value.onComplete(b)
|
||||
verify(providers[0]).cancelLoadTimeout()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLoadSuggested_onErrorRemovesTimeout() {
|
||||
val callback = object : ControlsBindingController.LoadCallback {
|
||||
override fun error(message: String) {}
|
||||
|
||||
override fun accept(t: List<Control>) {}
|
||||
}
|
||||
val subscription = mock(IControlsSubscription::class.java)
|
||||
|
||||
controller.bindAndLoadSuggested(TEST_COMPONENT_NAME_1, callback)
|
||||
|
||||
verify(providers[0]).maybeBindAndLoadSuggested(capture(subscriberCaptor))
|
||||
val b = Binder()
|
||||
subscriberCaptor.value.onSubscribe(b, subscription)
|
||||
|
||||
subscriberCaptor.value.onError(b, "")
|
||||
verify(providers[0]).cancelLoadTimeout()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBindService() {
|
||||
controller.bindService(TEST_COMPONENT_NAME_1)
|
||||
|
||||
@@ -80,6 +80,10 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
|
||||
@Captor
|
||||
private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo>
|
||||
|
||||
@Captor
|
||||
private lateinit var booleanConsumer: ArgumentCaptor<Consumer<Boolean>>
|
||||
|
||||
@Captor
|
||||
private lateinit var controlLoadCallbackCaptor:
|
||||
ArgumentCaptor<ControlsBindingController.LoadCallback>
|
||||
@@ -155,9 +159,13 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
verify(listingController).addCallback(capture(listingCallbackCaptor))
|
||||
}
|
||||
|
||||
private fun builderFromInfo(controlInfo: ControlInfo): Control.StatelessBuilder {
|
||||
private fun builderFromInfo(
|
||||
controlInfo: ControlInfo,
|
||||
structure: CharSequence = ""
|
||||
): Control.StatelessBuilder {
|
||||
return Control.StatelessBuilder(controlInfo.controlId, pendingIntent)
|
||||
.setDeviceType(controlInfo.deviceType).setTitle(controlInfo.controlTitle)
|
||||
.setStructure(structure)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -746,4 +754,70 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
inOrder.verify(persistenceWrapper).readFavorites()
|
||||
inOrder.verify(listingController).addCallback(listingCallbackCaptor.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSeedFavoritesForComponent() {
|
||||
var succeeded = false
|
||||
val control = builderFromInfo(TEST_CONTROL_INFO, TEST_STRUCTURE_INFO.structure).build()
|
||||
|
||||
controller.seedFavoritesForComponent(TEST_COMPONENT, Consumer { accepted ->
|
||||
succeeded = accepted
|
||||
})
|
||||
|
||||
verify(bindingController).bindAndLoadSuggested(eq(TEST_COMPONENT),
|
||||
capture(controlLoadCallbackCaptor))
|
||||
|
||||
controlLoadCallbackCaptor.value.accept(listOf(control))
|
||||
|
||||
delayableExecutor.runAllReady()
|
||||
|
||||
assertEquals(listOf(TEST_STRUCTURE_INFO),
|
||||
controller.getFavoritesForComponent(TEST_COMPONENT))
|
||||
assertTrue(succeeded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSeedFavoritesForComponent_error() {
|
||||
var succeeded = false
|
||||
|
||||
controller.seedFavoritesForComponent(TEST_COMPONENT, Consumer { accepted ->
|
||||
succeeded = accepted
|
||||
})
|
||||
|
||||
verify(bindingController).bindAndLoadSuggested(eq(TEST_COMPONENT),
|
||||
capture(controlLoadCallbackCaptor))
|
||||
|
||||
controlLoadCallbackCaptor.value.error("Error loading")
|
||||
|
||||
delayableExecutor.runAllReady()
|
||||
|
||||
assertEquals(listOf<StructureInfo>(), controller.getFavoritesForComponent(TEST_COMPONENT))
|
||||
assertFalse(succeeded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSeedFavoritesForComponent_inProgressCallback() {
|
||||
var succeeded = false
|
||||
var seeded = false
|
||||
val control = builderFromInfo(TEST_CONTROL_INFO, TEST_STRUCTURE_INFO.structure).build()
|
||||
|
||||
controller.seedFavoritesForComponent(TEST_COMPONENT, Consumer { accepted ->
|
||||
succeeded = accepted
|
||||
})
|
||||
|
||||
verify(bindingController).bindAndLoadSuggested(eq(TEST_COMPONENT),
|
||||
capture(controlLoadCallbackCaptor))
|
||||
|
||||
controller.addSeedingFavoritesCallback(Consumer { accepted ->
|
||||
seeded = accepted
|
||||
})
|
||||
controlLoadCallbackCaptor.value.accept(listOf(control))
|
||||
|
||||
delayableExecutor.runAllReady()
|
||||
|
||||
assertEquals(listOf(TEST_STRUCTURE_INFO),
|
||||
controller.getFavoritesForComponent(TEST_COMPONENT))
|
||||
assertTrue(succeeded)
|
||||
assertTrue(seeded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,22 @@ class ServiceWrapperTest : SysuiTestCase() {
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLoadSuggested_happyPath() {
|
||||
val result = wrapper.loadSuggested(subscriber)
|
||||
|
||||
assertTrue(result)
|
||||
verify(service).loadSuggested(subscriber)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLoadSuggested_error() {
|
||||
`when`(service.loadSuggested(any())).thenThrow(exception)
|
||||
val result = wrapper.loadSuggested(subscriber)
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSubscribe_happyPath() {
|
||||
val list = listOf("TEST_ID")
|
||||
|
||||
@@ -59,13 +59,15 @@ class StatefulControlSubscriberTest : SysuiTestCase() {
|
||||
|
||||
private lateinit var scs: StatefulControlSubscriber
|
||||
|
||||
private val REQUEST_LIMIT = 5L
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
`when`(provider.componentName).thenReturn(TEST_COMPONENT)
|
||||
`when`(provider.token).thenReturn(token)
|
||||
scs = StatefulControlSubscriber(controller, provider, executor)
|
||||
scs = StatefulControlSubscriber(controller, provider, executor, REQUEST_LIMIT)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -73,7 +75,7 @@ class StatefulControlSubscriberTest : SysuiTestCase() {
|
||||
scs.onSubscribe(token, subscription)
|
||||
|
||||
executor.runAllReady()
|
||||
verify(provider).startSubscription(subscription)
|
||||
verify(provider).startSubscription(subscription, REQUEST_LIMIT)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -81,7 +83,7 @@ class StatefulControlSubscriberTest : SysuiTestCase() {
|
||||
scs.onSubscribe(badToken, subscription)
|
||||
|
||||
executor.runAllReady()
|
||||
verify(provider, never()).startSubscription(subscription)
|
||||
verify(provider, never()).startSubscription(subscription, REQUEST_LIMIT)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user