Merge "Controls UI - Suggested controls" into rvc-dev am: 653604d01b

Change-Id: I833290dcd066e8e07c5a7d1955dbcc5dcfdea36d
This commit is contained in:
TreeHugger Robot
2020-03-18 22:34:44 +00:00
committed by Automerger Merge Worker
18 changed files with 478 additions and 120 deletions

View File

@@ -40,14 +40,20 @@
android:paddingBottom="8dp" /> android:paddingBottom="8dp" />
<TextView <TextView
style="@style/TextAppearance.ControlSetup.Title"
android:id="@+id/controls_title" android:id="@+id/controls_title"
android:text="@string/quick_controls_title" android:text="@string/quick_controls_title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:layout_gravity="center" />
android:layout_gravity="center"
android:textSize="25sp" <TextView
android:textColor="@*android:color/foreground_material_dark" style="@style/TextAppearance.ControlSetup.Subtitle"
android:fontFamily="@*android:string/config_headlineFontFamily" /> 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> </LinearLayout>
</merge> </merge>

View File

@@ -532,4 +532,8 @@
<!-- Respect drawable/rounded.xml intrinsic size for multiple radius corner path customization --> <!-- Respect drawable/rounded.xml intrinsic size for multiple radius corner path customization -->
<bool name="config_roundedCornerMultipleRadius">false</bool> <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> </resources>

View File

@@ -2657,4 +2657,8 @@
<!-- Tooltip to show in management screen when there are multiple structures [CHAR_LIMIT=50] --> <!-- 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> <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> </resources>

View File

@@ -699,6 +699,20 @@
<item name="*android:colorPopupBackground">@color/control_list_popup_background</item> <item name="*android:colorPopupBackground">@color/control_list_popup_background</item>
</style> </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"/> <style name="Theme.ControlsRequestDialog" parent="@style/Theme.SystemUI.MediaProjectionAlertDialog"/>
</resources> </resources>

View File

@@ -42,6 +42,14 @@ interface ControlsBindingController : UserAwareController {
*/ */
fun bindAndLoad(component: ComponentName, callback: LoadCallback): Runnable 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. * Request to bind to the given service.
* *

View File

@@ -44,6 +44,8 @@ open class ControlsBindingControllerImpl @Inject constructor(
companion object { companion object {
private const val TAG = "ControlsBindingControllerImpl" 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()) private var currentUser = UserHandle.of(ActivityManager.getCurrentUser())
@@ -97,24 +99,37 @@ open class ControlsBindingControllerImpl @Inject constructor(
component: ComponentName, component: ComponentName,
callback: ControlsBindingController.LoadCallback callback: ControlsBindingController.LoadCallback
): Runnable { ): Runnable {
val subscriber = LoadSubscriber(callback) val subscriber = LoadSubscriber(callback, MAX_CONTROLS_REQUEST)
retrieveLifecycleManager(component).maybeBindAndLoad(subscriber) retrieveLifecycleManager(component).maybeBindAndLoad(subscriber)
return subscriber.loadCancel() 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) { override fun subscribe(structureInfo: StructureInfo) {
// make sure this has happened. only allow one active subscription // make sure this has happened. only allow one active subscription
unsubscribe() unsubscribe()
statefulControlSubscriber = null
val provider = retrieveLifecycleManager(structureInfo.componentName) val provider = retrieveLifecycleManager(structureInfo.componentName)
val scs = StatefulControlSubscriber(lazyController.get(), provider, backgroundExecutor) val scs = StatefulControlSubscriber(
lazyController.get(),
provider,
backgroundExecutor,
MAX_CONTROLS_REQUEST
)
statefulControlSubscriber = scs statefulControlSubscriber = scs
provider.maybeBindAndSubscribe(structureInfo.controls.map { it.controlId }, scs) provider.maybeBindAndSubscribe(structureInfo.controls.map { it.controlId }, scs)
} }
override fun unsubscribe() { override fun unsubscribe() {
statefulControlSubscriber?.cancel() statefulControlSubscriber?.cancel()
statefulControlSubscriber = null
} }
override fun action( override fun action(
@@ -201,10 +216,11 @@ open class ControlsBindingControllerImpl @Inject constructor(
private inner class OnSubscribeRunnable( private inner class OnSubscribeRunnable(
token: IBinder, token: IBinder,
val subscription: IControlsSubscription val subscription: IControlsSubscription,
val requestLimit: Long
) : CallbackRunnable(token) { ) : CallbackRunnable(token) {
override fun doRun() { override fun doRun() {
provider?.startSubscription(subscription) provider?.startSubscription(subscription, requestLimit)
} }
} }
@@ -234,7 +250,8 @@ open class ControlsBindingControllerImpl @Inject constructor(
} }
private inner class LoadSubscriber( private inner class LoadSubscriber(
val callback: ControlsBindingController.LoadCallback val callback: ControlsBindingController.LoadCallback,
val requestLimit: Long
) : IControlsSubscriber.Stub() { ) : IControlsSubscriber.Stub() {
val loadedControls = ArrayList<Control>() val loadedControls = ArrayList<Control>()
var hasError = false var hasError = false
@@ -246,7 +263,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
override fun onSubscribe(token: IBinder, subs: IControlsSubscription) { override fun onSubscribe(token: IBinder, subs: IControlsSubscription) {
_loadCancelInternal = subs::cancel _loadCancelInternal = subs::cancel
backgroundExecutor.execute(OnSubscribeRunnable(token, subs)) backgroundExecutor.execute(OnSubscribeRunnable(token, subs, requestLimit))
} }
override fun onNext(token: IBinder, c: Control) { override fun onNext(token: IBinder, c: Control) {

View File

@@ -113,6 +113,25 @@ interface ControlsController : UserAwareController {
// FAVORITE MANAGEMENT // 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. * Get all the favorites.
* *

View File

@@ -31,6 +31,7 @@ import android.os.UserHandle
import android.provider.Settings import android.provider.Settings
import android.service.controls.Control import android.service.controls.Control
import android.service.controls.actions.ControlAction import android.service.controls.actions.ControlAction
import android.util.ArrayMap
import android.util.Log import android.util.Log
import com.android.internal.annotations.VisibleForTesting import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable import com.android.systemui.Dumpable
@@ -74,6 +75,9 @@ class ControlsControllerImpl @Inject constructor (
private var loadCanceller: Runnable? = null private var loadCanceller: Runnable? = null
private var seedingInProgress = false
private val seedingCallbacks = mutableListOf<Consumer<Boolean>>()
private var currentUser = UserHandle.of(ActivityManager.getCurrentUser()) private var currentUser = UserHandle.of(ActivityManager.getCurrentUser())
override val currentUserId override val currentUserId
get() = currentUser.identifier 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() { override fun cancelLoad() {
loadCanceller?.let { loadCanceller?.let {
executor.execute(it) executor.execute(it)

View File

@@ -66,22 +66,17 @@ class ControlsProviderLifecycleManager(
@GuardedBy("subscriptions") @GuardedBy("subscriptions")
private val subscriptions = mutableListOf<IControlsSubscription>() private val subscriptions = mutableListOf<IControlsSubscription>()
private var requiresBound = false private var requiresBound = false
@GuardedBy("queuedMessages") @GuardedBy("queuedServiceMethods")
private val queuedMessages: MutableSet<Message> = ArraySet() private val queuedServiceMethods: MutableSet<ServiceMethod> = ArraySet()
private var wrapper: ServiceWrapper? = null private var wrapper: ServiceWrapper? = null
private var bindTryCount = 0 private var bindTryCount = 0
private val TAG = javaClass.simpleName private val TAG = javaClass.simpleName
private var onLoadCanceller: Runnable? = null private var onLoadCanceller: Runnable? = null
companion object { 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 BIND_RETRY_DELAY = 1000L // ms
private const val LOAD_TIMEOUT_SECONDS = 30L // seconds private const val LOAD_TIMEOUT_SECONDS = 30L // seconds
private const val MAX_BIND_RETRIES = 5 private const val MAX_BIND_RETRIES = 5
private const val MAX_CONTROLS_REQUEST = 100000L
private const val DEBUG = true private const val DEBUG = true
private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or
Context.BIND_WAIVE_PRIORITY Context.BIND_WAIVE_PRIORITY
@@ -130,7 +125,7 @@ class ControlsProviderLifecycleManager(
try { try {
service.linkToDeath(this@ControlsProviderLifecycleManager, 0) service.linkToDeath(this@ControlsProviderLifecycleManager, 0)
} catch (_: RemoteException) {} } catch (_: RemoteException) {}
handlePendingMessages() handlePendingServiceMethods()
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
@@ -140,29 +135,14 @@ class ControlsProviderLifecycleManager(
} }
} }
private fun handlePendingMessages() { private fun handlePendingServiceMethods() {
val queue = synchronized(queuedMessages) { val queue = synchronized(queuedServiceMethods) {
ArraySet(queuedMessages).also { ArraySet(queuedServiceMethods).also {
queuedMessages.clear() queuedServiceMethods.clear()
} }
} }
if (Message.Unbind in queue) { queue.forEach {
bindService(false) it.run()
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)
} }
} }
@@ -177,33 +157,17 @@ class ControlsProviderLifecycleManager(
} }
} }
private fun queueMessage(message: Message) { private fun queueServiceMethod(sm: ServiceMethod) {
synchronized(queuedMessages) { synchronized(queuedServiceMethods) {
queuedMessages.add(message) queuedServiceMethods.add(sm)
} }
} }
private fun unqueueMessageType(type: Int) { private fun invokeOrQueue(sm: ServiceMethod) {
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) {
wrapper?.run { wrapper?.run {
f() sm.run()
} ?: run { } ?: run {
queueMessage(msg) queueServiceMethod(sm)
bindService(true) bindService(true)
} }
} }
@@ -217,7 +181,6 @@ class ControlsProviderLifecycleManager(
* @param subscriber the subscriber that manages coordination for loading controls * @param subscriber the subscriber that manages coordination for loading controls
*/ */
fun maybeBindAndLoad(subscriber: IControlsSubscriber.Stub) { fun maybeBindAndLoad(subscriber: IControlsSubscriber.Stub) {
unqueueMessageType(MSG_UNBIND)
onLoadCanceller = executor.executeDelayed({ onLoadCanceller = executor.executeDelayed({
// Didn't receive a response in time, log and send back error // Didn't receive a response in time, log and send back error
Log.d(TAG, "Timeout waiting onLoad for $componentName") Log.d(TAG, "Timeout waiting onLoad for $componentName")
@@ -225,7 +188,26 @@ class ControlsProviderLifecycleManager(
unbindService() unbindService()
}, LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS) }, 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() { fun cancelLoadTimeout() {
@@ -240,23 +222,8 @@ class ControlsProviderLifecycleManager(
* *
* @param controlIds a list of the ids of controls to send status back. * @param controlIds a list of the ids of controls to send status back.
*/ */
fun maybeBindAndSubscribe(controlIds: List<String>, subscriber: IControlsSubscriber) { fun maybeBindAndSubscribe(controlIds: List<String>, subscriber: IControlsSubscriber) =
invokeOrQueue( invokeOrQueue(Subscribe(controlIds, subscriber))
{ 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()
}
}
/** /**
* Request a call to [ControlsProviderService.performControlAction]. * 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 controlId the id of the [Control] the action is performed on
* @param action the action performed * @param action the action performed
*/ */
fun maybeBindAndSendAction(controlId: String, action: ControlAction) { fun maybeBindAndSendAction(controlId: String, action: ControlAction) =
invokeOrQueue({ action(controlId, action) }, Message.Action(controlId, action)) invokeOrQueue(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()
}
}
/** /**
* Starts the subscription to the [ControlsProviderService] and requests status of controls. * 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 * @param subscription the subscription to use to request controls
* @see maybeBindAndLoad * @see maybeBindAndLoad
*/ */
fun startSubscription(subscription: IControlsSubscription) { fun startSubscription(subscription: IControlsSubscription, requestLimit: Long) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "startSubscription: $subscription") Log.d(TAG, "startSubscription: $subscription")
} }
synchronized(subscriptions) { synchronized(subscriptions) {
subscriptions.add(subscription) subscriptions.add(subscription)
} }
wrapper?.request(subscription, MAX_CONTROLS_REQUEST) wrapper?.request(subscription, requestLimit)
} }
/** /**
@@ -316,7 +272,6 @@ class ControlsProviderLifecycleManager(
* Request bind to the service. * Request bind to the service.
*/ */
fun bindService() { fun bindService() {
unqueueMessageType(MSG_UNBIND)
bindService(true) 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 inner class ServiceMethod {
abstract val type: Int fun run() {
class Load(val subscriber: IControlsSubscriber.Stub) : Message() { if (!callWrapper()) {
override val type = MSG_LOAD 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
} }
} }
} }

View File

@@ -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 { fun subscribe(controlIds: List<String>, subscriber: IControlsSubscriber): Boolean {
return callThroughService { return callThroughService {
service.subscribe(controlIds, subscriber) service.subscribe(controlIds, subscriber)

View File

@@ -31,7 +31,8 @@ import com.android.systemui.util.concurrency.DelayableExecutor
class StatefulControlSubscriber( class StatefulControlSubscriber(
private val controller: ControlsController, private val controller: ControlsController,
private val provider: ControlsProviderLifecycleManager, private val provider: ControlsProviderLifecycleManager,
private val bgExecutor: DelayableExecutor private val bgExecutor: DelayableExecutor,
private val requestLimit: Long
) : IControlsSubscriber.Stub() { ) : IControlsSubscriber.Stub() {
private var subscriptionOpen = false private var subscriptionOpen = false
private var subscription: IControlsSubscription? = null private var subscription: IControlsSubscription? = null
@@ -50,7 +51,7 @@ class StatefulControlSubscriber(
run(token) { run(token) {
subscriptionOpen = true subscriptionOpen = true
subscription = subs subscription = subs
provider.startSubscription(subs) provider.startSubscription(subs, requestLimit)
} }
} }

View File

@@ -55,7 +55,7 @@ class ControlsRequestDialog @Inject constructor(
private lateinit var control: Control private lateinit var control: Control
private var dialog: Dialog? = null private var dialog: Dialog? = null
private val callback = object : ControlsListingController.ControlsListingCallback { private val callback = object : ControlsListingController.ControlsListingCallback {
override fun onServicesUpdated(candidates: List<ControlsServiceInfo>) {} override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {}
} }
private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) { private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {

View File

@@ -52,6 +52,7 @@ import com.android.systemui.R
import dagger.Lazy import dagger.Lazy
import java.text.Collator import java.text.Collator
import java.util.function.Consumer
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -89,6 +90,7 @@ class ControlsUiControllerImpl @Inject constructor (
private var popup: ListPopupWindow? = null private var popup: ListPopupWindow? = null
private var activeDialog: Dialog? = null private var activeDialog: Dialog? = null
private val addControlsItem: SelectionItem private val addControlsItem: SelectionItem
private var hidden = true
init { init {
val addDrawable = context.getDrawable(R.drawable.ic_add).apply { val addDrawable = context.getDrawable(R.drawable.ic_add).apply {
@@ -134,11 +136,15 @@ class ControlsUiControllerImpl @Inject constructor (
override fun show(parent: ViewGroup) { override fun show(parent: ViewGroup) {
Log.d(ControlsUiController.TAG, "show()") Log.d(ControlsUiController.TAG, "show()")
this.parent = parent this.parent = parent
hidden = false
allStructures = controlsController.get().getFavorites() allStructures = controlsController.get().getFavorites()
selectedStructure = loadPreference(allStructures) 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 // only show initial view if there are really no favorites across any structure
listingCallback = createCallback(::showInitialSetupView) listingCallback = createCallback(::showInitialSetupView)
} else { } else {
@@ -154,6 +160,20 @@ class ControlsUiControllerImpl @Inject constructor (
controlsListingController.get().addCallback(listingCallback) 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>) { private fun showInitialSetupView(items: List<SelectionItem>) {
parent.removeAllViews() parent.removeAllViews()
@@ -320,13 +340,14 @@ class ControlsUiControllerImpl @Inject constructor (
selectedStructure = newSelection selectedStructure = newSelection
updatePreferences(selectedStructure) updatePreferences(selectedStructure)
controlsListingController.get().removeCallback(listingCallback) controlsListingController.get().removeCallback(listingCallback)
show(parent) reload(parent)
} }
} }
} }
override fun hide() { override fun hide() {
Log.d(ControlsUiController.TAG, "hide()") Log.d(ControlsUiController.TAG, "hide()")
hidden = true
popup?.dismiss() popup?.dismiss()
activeDialog?.dismiss() activeDialog?.dismiss()

View File

@@ -31,11 +31,13 @@ import android.app.WallpaperManager;
import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManager;
import android.app.trust.TrustManager; import android.app.trust.TrustManager;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.UserInfo; import android.content.pm.UserInfo;
import android.content.res.Resources; import android.content.res.Resources;
import android.database.ContentObserver; import android.database.ContentObserver;
@@ -92,6 +94,8 @@ import com.android.systemui.MultiListLayout;
import com.android.systemui.MultiListLayout.MultiListAdapter; import com.android.systemui.MultiListLayout.MultiListAdapter;
import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.colorextraction.SysuiColorExtractor; 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.management.ControlsListingController;
import com.android.systemui.controls.ui.ControlsUiController; import com.android.systemui.controls.ui.ControlsUiController;
import com.android.systemui.dagger.qualifiers.Background; 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_EMERGENCY = "emergency";
private static final String GLOBAL_ACTION_KEY_SCREENSHOT = "screenshot"; 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 Context mContext;
private final GlobalActionsManager mWindowManagerFuncs; private final GlobalActionsManager mWindowManagerFuncs;
private final AudioManager mAudioManager; private final AudioManager mAudioManager;
@@ -215,7 +222,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener,
NotificationShadeWindowController notificationShadeWindowController, NotificationShadeWindowController notificationShadeWindowController,
ControlsUiController controlsUiController, IWindowManager iWindowManager, ControlsUiController controlsUiController, IWindowManager iWindowManager,
@Background Executor backgroundExecutor, @Background Executor backgroundExecutor,
ControlsListingController controlsListingController) { ControlsListingController controlsListingController,
ControlsController controlsController) {
mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme); mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme);
mWindowManagerFuncs = windowManagerFuncs; mWindowManagerFuncs = windowManagerFuncs;
mAudioManager = audioManager; 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) * Show the global actions dialog (creating if necessary)
* *

View File

@@ -206,6 +206,56 @@ class ControlsBindingControllerImplTest : SysuiTestCase() {
verify(subscription, never()).cancel() 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 @Test
fun testBindService() { fun testBindService() {
controller.bindService(TEST_COMPONENT_NAME_1) controller.bindService(TEST_COMPONENT_NAME_1)

View File

@@ -80,6 +80,10 @@ class ControlsControllerImplTest : SysuiTestCase() {
@Captor @Captor
private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo> private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo>
@Captor
private lateinit var booleanConsumer: ArgumentCaptor<Consumer<Boolean>>
@Captor @Captor
private lateinit var controlLoadCallbackCaptor: private lateinit var controlLoadCallbackCaptor:
ArgumentCaptor<ControlsBindingController.LoadCallback> ArgumentCaptor<ControlsBindingController.LoadCallback>
@@ -155,9 +159,13 @@ class ControlsControllerImplTest : SysuiTestCase() {
verify(listingController).addCallback(capture(listingCallbackCaptor)) 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) return Control.StatelessBuilder(controlInfo.controlId, pendingIntent)
.setDeviceType(controlInfo.deviceType).setTitle(controlInfo.controlTitle) .setDeviceType(controlInfo.deviceType).setTitle(controlInfo.controlTitle)
.setStructure(structure)
} }
@Test @Test
@@ -746,4 +754,70 @@ class ControlsControllerImplTest : SysuiTestCase() {
inOrder.verify(persistenceWrapper).readFavorites() inOrder.verify(persistenceWrapper).readFavorites()
inOrder.verify(listingController).addCallback(listingCallbackCaptor.value) 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)
}
} }

View File

@@ -91,6 +91,22 @@ class ServiceWrapperTest : SysuiTestCase() {
assertFalse(result) 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 @Test
fun testSubscribe_happyPath() { fun testSubscribe_happyPath() {
val list = listOf("TEST_ID") val list = listOf("TEST_ID")

View File

@@ -59,13 +59,15 @@ class StatefulControlSubscriberTest : SysuiTestCase() {
private lateinit var scs: StatefulControlSubscriber private lateinit var scs: StatefulControlSubscriber
private val REQUEST_LIMIT = 5L
@Before @Before
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
`when`(provider.componentName).thenReturn(TEST_COMPONENT) `when`(provider.componentName).thenReturn(TEST_COMPONENT)
`when`(provider.token).thenReturn(token) `when`(provider.token).thenReturn(token)
scs = StatefulControlSubscriber(controller, provider, executor) scs = StatefulControlSubscriber(controller, provider, executor, REQUEST_LIMIT)
} }
@Test @Test
@@ -73,7 +75,7 @@ class StatefulControlSubscriberTest : SysuiTestCase() {
scs.onSubscribe(token, subscription) scs.onSubscribe(token, subscription)
executor.runAllReady() executor.runAllReady()
verify(provider).startSubscription(subscription) verify(provider).startSubscription(subscription, REQUEST_LIMIT)
} }
@Test @Test
@@ -81,7 +83,7 @@ class StatefulControlSubscriberTest : SysuiTestCase() {
scs.onSubscribe(badToken, subscription) scs.onSubscribe(badToken, subscription)
executor.runAllReady() executor.runAllReady()
verify(provider, never()).startSubscription(subscription) verify(provider, never()).startSubscription(subscription, REQUEST_LIMIT)
} }
@Test @Test