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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 {
return callThroughService {
service.subscribe(controlIds, subscriber)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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