Add flag to allowing the system to make BubbleMetadata for a notification

- Add experiment class to try and keep experimented related code in
- If notification has info to bubble & is allowed, make BubbleMetadata for
  it (but don't actually bubble)
- Options for only allowing messages to bubble
- Alter NotificationEntry to have settable BubbleMetadata
- Add notion of user created bubbles & user promoting / demoting content
  to a bubble; user created bubbles can't be canceled or finished, the
  user must explictly dismiss the notif
- Move all of our Dependency.get into constructor & use inject

Test: it compiles (see manual tests in CL with the menu affordance)
Test: atest BubbleControllerTest
Bug: 138116133
Bug: 143173197
Change-Id: I6100bce7b74146afd4e0c4c02d4ce7731fecd5af
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 0231b56..9f7bdd4 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -67,7 +67,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -82,6 +81,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationData;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBarWindowController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ZenModeController;
@@ -96,6 +96,8 @@
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
+import dagger.Lazy;
+
 /**
  * Bubbles are a special type of content that can "float" on top of other apps or System UI.
  * Bubbles can be expanded to show more content.
@@ -132,6 +134,7 @@
     private BubbleExpandListener mExpandListener;
     @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
     private final NotificationGroupManager mNotificationGroupManager;
+    private final Lazy<ShadeController> mShadeController;
 
     private BubbleData mBubbleData;
     @Nullable private BubbleStackView mStackView;
@@ -206,24 +209,34 @@
     }
 
     @Inject
-    public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
-            BubbleData data, ConfigurationController configurationController,
-            NotificationInterruptionStateProvider interruptionStateProvider,
-            ZenModeController zenModeController,
-            NotificationLockscreenUserManager notifUserManager,
-            NotificationGroupManager groupManager) {
-        this(context, statusBarWindowController, data, null /* synchronizer */,
-                configurationController, interruptionStateProvider, zenModeController,
-                notifUserManager, groupManager);
-    }
-
-    public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
-            BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
+    public BubbleController(Context context,
+            StatusBarWindowController statusBarWindowController,
+            StatusBarStateController statusBarStateController,
+            Lazy<ShadeController> shadeController,
+            BubbleData data,
             ConfigurationController configurationController,
             NotificationInterruptionStateProvider interruptionStateProvider,
             ZenModeController zenModeController,
             NotificationLockscreenUserManager notifUserManager,
-            NotificationGroupManager groupManager) {
+            NotificationGroupManager groupManager,
+            NotificationEntryManager entryManager) {
+        this(context, statusBarWindowController, statusBarStateController, shadeController,
+                data, null /* synchronizer */, configurationController, interruptionStateProvider,
+                zenModeController, notifUserManager, groupManager, entryManager);
+    }
+
+    public BubbleController(Context context,
+            StatusBarWindowController statusBarWindowController,
+            StatusBarStateController statusBarStateController,
+            Lazy<ShadeController> shadeController,
+            BubbleData data,
+            @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
+            ConfigurationController configurationController,
+            NotificationInterruptionStateProvider interruptionStateProvider,
+            ZenModeController zenModeController,
+            NotificationLockscreenUserManager notifUserManager,
+            NotificationGroupManager groupManager,
+            NotificationEntryManager entryManager) {
         mContext = context;
         mNotificationInterruptionStateProvider = interruptionStateProvider;
         mNotifUserManager = notifUserManager;
@@ -249,7 +262,7 @@
         mBubbleData = data;
         mBubbleData.setListener(mBubbleDataListener);
 
-        mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
+        mNotificationEntryManager = entryManager;
         mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
         mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor);
         mNotificationGroupManager = groupManager;
@@ -271,9 +284,10 @@
                     }
                 });
 
+        mShadeController = shadeController;
         mStatusBarWindowController = statusBarWindowController;
         mStatusBarStateListener = new StatusBarStateListener();
-        Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener);
+        statusBarStateController.addCallback(mStatusBarStateListener);
 
         mTaskStackListener = new BubbleTaskStackListener();
         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
@@ -497,15 +511,45 @@
      * @param notif the notification associated with this bubble.
      */
     void updateBubble(NotificationEntry notif) {
-        updateBubble(notif, /* supressFlyout */ false);
+        updateBubble(notif, false /* suppressFlyout */);
     }
 
     void updateBubble(NotificationEntry notif, boolean suppressFlyout) {
+        updateBubble(notif, suppressFlyout, true /* showInShade */);
+    }
+
+    void updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade) {
         // If this is an interruptive notif, mark that it's interrupted
         if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) {
             notif.setInterruption();
         }
-        mBubbleData.notificationEntryUpdated(notif, suppressFlyout);
+        mBubbleData.notificationEntryUpdated(notif, suppressFlyout, showInShade);
+    }
+
+    /**
+     * Called when a user has indicated that an active notification should be shown as a bubble.
+     * <p>
+     * This method will collapse the shade, create the bubble without a flyout or dot, and suppress
+     * the notification from appearing in the shade.
+     *
+     * @param entry the notification to show as a bubble.
+     */
+    public void onUserCreatedBubbleFromNotification(NotificationEntry entry) {
+        mShadeController.get().collapsePanel(true);
+        entry.setFlagBubble(true);
+        updateBubble(entry, true /* suppressFlyout */, false /* showInShade */);
+        mBubbleData.getBubbleWithKey(entry.getKey()).setUserCreated(true);
+    }
+
+    /**
+     * Called when a user has indicated that an active notification appearing as a bubble should
+     * no longer be shown as a bubble.
+     *
+     * @param entry the notification to no longer show as a bubble.
+     */
+    public void onUserDemotedBubbleFromNotification(NotificationEntry entry) {
+        entry.setFlagBubble(false);
+        removeBubble(entry.getKey(), DISMISS_BLOCKED);
     }
 
     /**
@@ -571,7 +615,7 @@
                     mNotificationEntryManager.updateNotifications(
                             "BubbleController.onNotificationRemoveRequested");
                     return true;
-                } else if (!userRemovedNotif && entry != null) {
+                } else if (!userRemovedNotif && entry != null && !bubble.isUserCreated()) {
                     // This wasn't a user removal so we should remove the bubble as well
                     mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL);
                     return false;
@@ -631,6 +675,9 @@
     private final NotificationEntryListener mEntryListener = new NotificationEntryListener() {
         @Override
         public void onPendingEntryAdded(NotificationEntry entry) {
+            Bubble b = mBubbleData.getBubbleWithKey(entry.getKey());
+            BubbleExperimentConfig.adjustForExperiments(mContext, entry, b);
+
             if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
                     && canLaunchInActivityView(mContext, entry)) {
                 updateBubble(entry);
@@ -639,13 +686,15 @@
 
         @Override
         public void onPreEntryUpdated(NotificationEntry entry) {
+            Bubble b = mBubbleData.getBubbleWithKey(entry.getKey());
+            BubbleExperimentConfig.adjustForExperiments(mContext, entry, b);
+
             boolean shouldBubble = mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
                     && canLaunchInActivityView(mContext, entry);
             if (!shouldBubble && mBubbleData.hasBubbleWithKey(entry.getKey())) {
                 // It was previously a bubble but no longer a bubble -- lets remove it
                 removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
             } else if (shouldBubble) {
-                Bubble b = mBubbleData.getBubbleWithKey(entry.getKey());
                 updateBubble(entry);
             }
         }
@@ -949,19 +998,26 @@
         PendingIntent intent = entry.getBubbleMetadata() != null
                 ? entry.getBubbleMetadata().getIntent()
                 : null;
+        return canLaunchIntentInActivityView(context, entry, intent);
+    }
+
+    static boolean canLaunchIntentInActivityView(Context context, NotificationEntry entry,
+            PendingIntent intent) {
         if (intent == null) {
-            Log.w(TAG, "Unable to create bubble -- no intent");
+            Log.w(TAG, "Unable to create bubble -- no intent: " + entry.getKey());
             return false;
         }
         ActivityInfo info =
                 intent.getIntent().resolveActivityInfo(context.getPackageManager(), 0);
         if (info == null) {
-            Log.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: "
+            Log.w(TAG, "Unable to send as bubble, "
+                    + entry.getKey() + " couldn't find activity info for intent: "
                     + intent);
             return false;
         }
         if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
-            Log.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: "
+            Log.w(TAG, "Unable to send as bubble, "
+                    + entry.getKey() + " activity is not resizable for intent: "
                     + intent);
             return false;
         }