diff --git a/packages/SystemUI/res/layout/controls_no_favorites.xml b/packages/SystemUI/res/layout/controls_no_favorites.xml
index 8074efd18492e..74fc167c4632d 100644
--- a/packages/SystemUI/res/layout/controls_no_favorites.xml
+++ b/packages/SystemUI/res/layout/controls_no_favorites.xml
@@ -40,14 +40,20 @@
android:paddingBottom="8dp" />
+ android:layout_gravity="center" />
+
+
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 06e027d7cae7d..4d6b759c86e44 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -532,4 +532,8 @@
false
+
+
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 93bafdbee91db..cb08840d02c83 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2657,4 +2657,8 @@
Swipe to see other structures
+
+
+ Loading recommendations
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 20b88a1a550df..1283fe0a9181f 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -699,6 +699,20 @@
- @color/control_list_popup_background
+
+
+
+
+
+
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 c5af436e7e925..d4d4d2a7d8fe2 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
@@ -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.
*
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 f8d4a39a98fea..5d03fc51004d7 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
@@ -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()
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) {
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 9e0d26c3935f3..ae75dd4d94ab3 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
@@ -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
+ )
+
+ /**
+ * 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
+
/**
* Get all the favorites.
*
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 9cb902f51f22a..2e34ea55ebe42 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -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>()
+
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 {
+ 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
+ ) {
+ 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) {
+ executor.execute {
+ val structureToControls =
+ ArrayMap>()
+
+ controls.forEach {
+ val structure = it.structure ?: ""
+ val list = structureToControls.get(structure)
+ ?: mutableListOf()
+ 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)
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 4918bd7b33493..209d056260a9e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
@@ -66,22 +66,17 @@ class ControlsProviderLifecycleManager(
@GuardedBy("subscriptions")
private val subscriptions = mutableListOf()
private var requiresBound = false
- @GuardedBy("queuedMessages")
- private val queuedMessages: MutableSet = ArraySet()
+ @GuardedBy("queuedServiceMethods")
+ private val queuedServiceMethods: MutableSet = 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, subscriber: IControlsSubscriber) {
- invokeOrQueue(
- { subscribe(controlIds, subscriber) },
- Message.Subscribe(controlIds, subscriber)
- )
- }
-
- private fun subscribe(controlIds: List, 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, 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, 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,
+ 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
}
}
}
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 b2afd3c144d1f..2c717f5af8da2 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt
@@ -50,6 +50,12 @@ class ServiceWrapper(val service: IControlsProvider) {
}
}
+ fun loadSuggested(subscriber: IControlsSubscriber): Boolean {
+ return callThroughService {
+ service.loadSuggested(subscriber)
+ }
+ }
+
fun subscribe(controlIds: List, subscriber: IControlsSubscriber): Boolean {
return callThroughService {
service.subscribe(controlIds, subscriber)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/StatefulControlSubscriber.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/StatefulControlSubscriber.kt
index a371aa64df6e5..e3eabff49d59e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/StatefulControlSubscriber.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/StatefulControlSubscriber.kt
@@ -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)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt
index a7fc2ac800702..74a6c87d754b2 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt
@@ -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) {}
+ override fun onServicesUpdated(serviceInfos: List) {}
}
private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 138cd47caae76..ffae4653eba80 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -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 { _ -> 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) {
+ parent.removeAllViews()
+
+ val inflater = LayoutInflater.from(context)
+ inflater.inflate(R.layout.controls_no_favorites, parent, true)
+ val subtitle = parent.requireViewById(R.id.controls_subtitle)
+ subtitle.setVisibility(View.VISIBLE)
+ }
+
private fun showInitialSetupView(items: List) {
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()
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index b99d7655c425f..73539f9380e6a 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -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)
*
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 c25d4e2d4b308..2ff2b2a4eac76 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
@@ -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) {}
+ }
+ 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) {}
+ }
+ 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) {}
+ }
+ 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)
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 f9c98157edb82..971d14ceafbf2 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
@@ -80,6 +80,10 @@ class ControlsControllerImplTest : SysuiTestCase() {
@Captor
private lateinit var structureInfoCaptor: ArgumentCaptor
+
+ @Captor
+ private lateinit var booleanConsumer: ArgumentCaptor>
+
@Captor
private lateinit var controlLoadCallbackCaptor:
ArgumentCaptor
@@ -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(), 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)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ServiceWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ServiceWrapperTest.kt
index cd82844fcc50c..789d6dfebf72a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ServiceWrapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ServiceWrapperTest.kt
@@ -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")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/StatefulControlSubscriberTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/StatefulControlSubscriberTest.kt
index ff5c8d44eee3f..267520ef7f1d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/StatefulControlSubscriberTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/StatefulControlSubscriberTest.kt
@@ -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