Merge "Controls UI - Add 'reset' option for seeding" into rvc-dev
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 5d03fc5..7e8fec7 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
@@ -45,7 +45,7 @@
     companion object {
         private const val TAG = "ControlsBindingControllerImpl"
         private const val MAX_CONTROLS_REQUEST = 100000L
-        private const val SUGGESTED_CONTROLS_REQUEST = 4L
+        private const val SUGGESTED_CONTROLS_REQUEST = 6L
     }
 
     private var currentUser = UserHandle.of(ActivityManager.getCurrentUser())
@@ -61,6 +61,11 @@
      */
     private var statefulControlSubscriber: StatefulControlSubscriber? = null
 
+    /*
+     * Will track any active load subscriber. Only one can be active at any time.
+     */
+    private var loadSubscriber: LoadSubscriber? = null
+
     private val actionCallbackService = object : IControlsActionCallback.Stub() {
         override fun accept(
             token: IBinder,
@@ -99,17 +104,24 @@
         component: ComponentName,
         callback: ControlsBindingController.LoadCallback
     ): Runnable {
-        val subscriber = LoadSubscriber(callback, MAX_CONTROLS_REQUEST)
-        retrieveLifecycleManager(component).maybeBindAndLoad(subscriber)
-        return subscriber.loadCancel()
+        loadSubscriber?.loadCancel()
+
+        val ls = LoadSubscriber(callback, MAX_CONTROLS_REQUEST)
+        loadSubscriber = ls
+
+        retrieveLifecycleManager(component).maybeBindAndLoad(ls)
+        return ls.loadCancel()
     }
 
     override fun bindAndLoadSuggested(
         component: ComponentName,
         callback: ControlsBindingController.LoadCallback
     ) {
-        val subscriber = LoadSubscriber(callback, SUGGESTED_CONTROLS_REQUEST)
-        retrieveLifecycleManager(component).maybeBindAndLoadSuggested(subscriber)
+        loadSubscriber?.loadCancel()
+        val ls = LoadSubscriber(callback, SUGGESTED_CONTROLS_REQUEST)
+        loadSubscriber = ls
+
+        retrieveLifecycleManager(component).maybeBindAndLoadSuggested(ls)
     }
 
     override fun subscribe(structureInfo: StructureInfo) {
@@ -152,13 +164,16 @@
     override fun changeUser(newUser: UserHandle) {
         if (newUser == currentUser) return
 
-        unsubscribe()
         unbind()
-        currentProvider = null
         currentUser = newUser
     }
 
     private fun unbind() {
+        unsubscribe()
+
+        loadSubscriber?.loadCancel()
+        loadSubscriber = null
+
         currentProvider?.unbindService()
         currentProvider = null
     }
@@ -210,6 +225,20 @@
         val callback: ControlsBindingController.LoadCallback
     ) : CallbackRunnable(token) {
         override fun doRun() {
+            Log.d(TAG, "LoadSubscription: Complete and loading controls")
+            callback.accept(list)
+        }
+    }
+
+    private inner class OnCancelAndLoadRunnable(
+        token: IBinder,
+        val list: List<Control>,
+        val subscription: IControlsSubscription,
+        val callback: ControlsBindingController.LoadCallback
+    ) : CallbackRunnable(token) {
+        override fun doRun() {
+            Log.d(TAG, "LoadSubscription: Canceling and loading controls")
+            provider?.cancelSubscription(subscription)
             callback.accept(list)
         }
     }
@@ -220,6 +249,7 @@
         val requestLimit: Long
     ) : CallbackRunnable(token) {
         override fun doRun() {
+            Log.d(TAG, "LoadSubscription: Starting subscription")
             provider?.startSubscription(subscription, requestLimit)
         }
     }
@@ -254,34 +284,54 @@
         val requestLimit: Long
     ) : IControlsSubscriber.Stub() {
         val loadedControls = ArrayList<Control>()
-        var hasError = false
+        private var isTerminated = false
         private var _loadCancelInternal: (() -> Unit)? = null
+        private lateinit var subscription: IControlsSubscription
+
         fun loadCancel() = Runnable {
-                Log.d(TAG, "Cancel load requested")
-                _loadCancelInternal?.invoke()
-            }
+            Log.d(TAG, "Cancel load requested")
+            _loadCancelInternal?.invoke()
+        }
 
         override fun onSubscribe(token: IBinder, subs: IControlsSubscription) {
-            _loadCancelInternal = subs::cancel
+            subscription = subs
+            _loadCancelInternal = { currentProvider?.cancelSubscription(subscription) }
             backgroundExecutor.execute(OnSubscribeRunnable(token, subs, requestLimit))
         }
 
         override fun onNext(token: IBinder, c: Control) {
-            backgroundExecutor.execute { loadedControls.add(c) }
+            backgroundExecutor.execute {
+                if (isTerminated) return@execute
+
+                loadedControls.add(c)
+
+                // Once we have reached our requestLimit, send a request to cancel, and immediately
+                // load the results. Calls to onError() and onComplete() are not required after
+                // cancel.
+                if (loadedControls.size >= requestLimit) {
+                    maybeTerminateAndRun(
+                        OnCancelAndLoadRunnable(token, loadedControls, subscription, callback)
+                    )
+                }
+            }
         }
+
         override fun onError(token: IBinder, s: String) {
-            hasError = true
-            _loadCancelInternal = {}
-            currentProvider?.cancelLoadTimeout()
-            backgroundExecutor.execute(OnLoadErrorRunnable(token, s, callback))
+            maybeTerminateAndRun(OnLoadErrorRunnable(token, s, callback))
         }
 
         override fun onComplete(token: IBinder) {
+            maybeTerminateAndRun(OnLoadRunnable(token, loadedControls, callback))
+        }
+
+        private fun maybeTerminateAndRun(postTerminateFn: Runnable) {
+            if (isTerminated) return
+
+            isTerminated = true
             _loadCancelInternal = {}
-            if (!hasError) {
-                currentProvider?.cancelLoadTimeout()
-                backgroundExecutor.execute(OnLoadRunnable(token, loadedControls, callback))
-            }
+            currentProvider?.cancelLoadTimeout()
+
+            backgroundExecutor.execute(postTerminateFn)
         }
     }
 }
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 ae75dd4..568fb28 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
@@ -180,6 +180,11 @@
     fun countFavoritesForComponent(componentName: ComponentName): Int
 
     /**
+     * TEMPORARY for testing
+     */
+    fun resetFavorites()
+
+    /**
      * Interface for structure to pass data to [ControlsFavoritingActivity].
      */
     interface LoadData {
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 3483339..8805694 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -365,6 +365,8 @@
         componentName: ComponentName,
         callback: Consumer<Boolean>
     ) {
+        if (seedingInProgress) return
+
         Log.i(TAG, "Beginning request to seed favorites for: $componentName")
         if (!confirmAvailability()) {
             if (userChanging) {
@@ -495,6 +497,13 @@
         }
     }
 
+    override fun resetFavorites() {
+        executor.execute {
+            Favorites.clear()
+            persistenceWrapper.storeFavorites(Favorites.getAllStructures())
+        }
+    }
+
     override fun refreshStatus(componentName: ComponentName, control: Control) {
         if (!confirmAvailability()) {
             Log.d(TAG, "Controls not available")
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 895f1d2..a6af6a1 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
@@ -63,8 +63,6 @@
 ) : IBinder.DeathRecipient {
 
     val token: IBinder = Binder()
-    @GuardedBy("subscriptions")
-    private val subscriptions = mutableListOf<IControlsSubscription>()
     private var requiresBound = false
     @GuardedBy("queuedServiceMethods")
     private val queuedServiceMethods: MutableSet<ServiceMethod> = ArraySet()
@@ -194,7 +192,7 @@
      * 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.
+     * The service will be unbound if the call times out.
      *
      * @param subscriber the subscriber that manages coordination for loading controls
      */
@@ -245,9 +243,7 @@
         if (DEBUG) {
             Log.d(TAG, "startSubscription: $subscription")
         }
-        synchronized(subscriptions) {
-            subscriptions.add(subscription)
-        }
+
         wrapper?.request(subscription, requestLimit)
     }
 
@@ -261,9 +257,7 @@
         if (DEBUG) {
             Log.d(TAG, "cancelSubscription: $subscription")
         }
-        synchronized(subscriptions) {
-            subscriptions.remove(subscription)
-        }
+
         wrapper?.cancel(subscription)
     }
 
@@ -281,17 +275,6 @@
         onLoadCanceller?.run()
         onLoadCanceller = null
 
-        // be sure to cancel all subscriptions
-        val subs = synchronized(subscriptions) {
-            ArrayList(subscriptions).also {
-                subscriptions.clear()
-            }
-        }
-
-        subs.forEach {
-            wrapper?.cancel(it)
-        }
-
         bindService(false)
     }
 
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 208d911..a5f4298 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -16,18 +16,26 @@
 
 package com.android.systemui.controls.ui
 
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ObjectAnimator
+import android.app.AlertDialog
 import android.app.Dialog
 import android.content.ComponentName
 import android.content.Context
+import android.content.DialogInterface
 import android.content.Intent
 import android.content.SharedPreferences
 import android.content.res.Configuration
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.LayerDrawable
+import android.os.Process
 import android.service.controls.Control
 import android.service.controls.actions.ControlAction
 import android.util.TypedValue
 import android.util.Log
+import android.view.animation.AccelerateInterpolator
+import android.view.animation.DecelerateInterpolator
 import android.view.ContextThemeWrapper
 import android.view.LayoutInflater
 import android.view.View
@@ -77,6 +85,8 @@
         private const val PREF_COMPONENT = "controls_component"
         private const val PREF_STRUCTURE = "controls_structure"
 
+        private const val FADE_IN_MILLIS = 225L
+
         private val EMPTY_COMPONENT = ComponentName("", "")
         private val EMPTY_STRUCTURE = StructureInfo(
             EMPTY_COMPONENT,
@@ -153,7 +163,20 @@
 
     private fun reload(parent: ViewGroup) {
         if (hidden) return
-        show(parent)
+
+        val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f)
+        fadeAnim.setInterpolator(AccelerateInterpolator(1.0f))
+        fadeAnim.setDuration(FADE_IN_MILLIS)
+        fadeAnim.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                show(parent)
+                val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f)
+                showAnim.setInterpolator(DecelerateInterpolator(1.0f))
+                showAnim.setDuration(FADE_IN_MILLIS)
+                showAnim.start()
+            }
+        })
+        fadeAnim.start()
     }
 
     private fun showSeedingView(items: List<SelectionItem>) {
@@ -229,7 +252,8 @@
 
     private fun createMenu() {
         val items = arrayOf(
-            context.resources.getString(R.string.controls_menu_add)
+            context.resources.getString(R.string.controls_menu_add),
+            "Reset"
         )
         var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items)
 
@@ -249,6 +273,8 @@
                             when (pos) {
                                 // 0: Add Control
                                 0 -> startFavoritingActivity(view.context, selectedStructure)
+                                // 1: TEMPORARY for reset controls
+                                1 -> showResetConfirmation()
                                 else -> Log.w(ControlsUiController.TAG,
                                     "Unsupported index ($pos) on 'more' menu selection")
                             }
@@ -275,6 +301,39 @@
         })
     }
 
+    private fun showResetConfirmation() {
+        val builder = AlertDialog.Builder(
+            context,
+            android.R.style.Theme_DeviceDefault_Dialog_Alert
+        ).apply {
+            setMessage("For testing purposes: Would you like to " +
+                "reset your favorited device controls?")
+            setPositiveButton(
+                android.R.string.ok,
+                DialogInterface.OnClickListener { dialog, _ ->
+                    val userHandle = Process.myUserHandle()
+                    val userContext = context.createContextAsUser(userHandle, 0)
+                    val prefs = userContext.getSharedPreferences(
+                        "controls_prefs", Context.MODE_PRIVATE)
+                    prefs.edit().putBoolean("ControlsSeedingCompleted", false).apply()
+                    controlsController.get().resetFavorites()
+                    dialog.dismiss()
+                    context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
+            })
+            setNegativeButton(
+                android.R.string.cancel,
+                DialogInterface.OnClickListener {
+                    dialog, _ -> dialog.cancel()
+                }
+            )
+        }
+        builder.create().apply {
+            getWindow().apply {
+                setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
+            }
+        }.show()
+    }
+
     private fun createDropDown(items: List<SelectionItem>) {
         items.forEach {
             RenderInfo.registerComponentIcon(it.componentName, it.icon)
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index 4dd5e87..79def1d 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -205,7 +205,9 @@
     private final IWindowManager mIWindowManager;
     private final Executor mBackgroundExecutor;
     private final ControlsListingController mControlsListingController;
-    private boolean mAnyControlsProviders = false;
+    private List<ControlsServiceInfo> mControlsServiceInfos = new ArrayList<>();
+    private ControlsController mControlsController;
+    private SharedPreferences mControlsPreferences;
 
     @VisibleForTesting
     public enum GlobalActionsEvent implements UiEventLogger.UiEventEnum {
@@ -271,6 +273,7 @@
         mBackgroundExecutor = backgroundExecutor;
         mControlsListingController = controlsListingController;
         mBlurUtils = blurUtils;
+        mControlsController = controlsController;
 
         // receive broadcasts
         IntentFilter filter = new IntentFilter();
@@ -309,45 +312,54 @@
             }
         });
 
-        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();
-                        }
-                );
-            }
+            mControlsServiceInfos = list;
         });
+
+        // Need to be user-specific with the context to make sure we read the correct prefs
+        Context userContext = context.createContextAsUser(
+                new UserHandle(mUserManager.getUserHandle()), 0);
+        mControlsPreferences = userContext.getSharedPreferences(PREFS_CONTROLS_FILE,
+            Context.MODE_PRIVATE);
+
     }
 
+    private void seedFavorites() {
+        if (mControlsServiceInfos.isEmpty()
+                || mControlsController.getFavorites().size() > 0
+                || mControlsPreferences.getBoolean(PREFS_CONTROLS_SEEDING_COMPLETED, false)) {
+            return;
+        }
 
+        /*
+         * 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.
+         */
+        String preferredControlsPackage = mContext.getResources()
+                .getString(com.android.systemui.R.string.config_controlsPreferredPackage);
 
+        ComponentName preferredComponent = null;
+        for (ControlsServiceInfo info : mControlsServiceInfos) {
+            if (info.componentName.getPackageName().equals(preferredControlsPackage)) {
+                preferredComponent = info.componentName;
+                break;
+            }
+        }
+
+        if (preferredComponent == null) {
+            Log.i(TAG, "Controls seeding: No preferred component has been set, will not seed");
+            mControlsPreferences.edit().putBoolean(PREFS_CONTROLS_SEEDING_COMPLETED, true).apply();
+        }
+
+        mControlsController.seedFavoritesForComponent(
+                preferredComponent,
+                (accepted) -> {
+                    Log.i(TAG, "Controls seeded: " + accepted);
+                    mControlsPreferences.edit().putBoolean(PREFS_CONTROLS_SEEDING_COMPLETED,
+                        accepted).apply();
+                });
+    }
 
     /**
      * Show the global actions dialog (creating if necessary)
@@ -393,6 +405,7 @@
         awakenIfNecessary();
         mDialog = createDialog();
         prepareDialog();
+        seedFavorites();
 
         // If we only have 1 item and it's a simple press action, just do this action.
         if (mAdapter.getCount() == 1
@@ -2017,6 +2030,6 @@
     private boolean shouldShowControls() {
         return mKeyguardStateController.isUnlocked()
                 && mControlsUiController.getAvailable()
-                && mAnyControlsProviders;
+                && !mControlsServiceInfos.isEmpty();
     }
 }
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 88316f2..bb003ee 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
@@ -125,7 +125,7 @@
         loadSubscriberCaptor.value.onSubscribe(Binder(), subscription)
 
         canceller.run()
-        verify(subscription).cancel()
+        verify(providers[0]).cancelSubscription(subscription)
     }
 
     @Test
@@ -145,7 +145,7 @@
 
         loadSubscriberCaptor.value.onComplete(b)
         canceller.run()
-        verify(subscription, never()).cancel()
+        verify(providers[0], never()).cancelSubscription(subscription)
     }
 
     @Test
@@ -203,7 +203,7 @@
 
         loadSubscriberCaptor.value.onError(b, "")
         canceller.run()
-        verify(subscription, never()).cancel()
+        verify(providers[0], never()).cancelSubscription(subscription)
     }
 
     @Test