Add logging for bubble UI events.

Bug: 123543171
Test: manual on local device using statsd_testdrive
Change-Id: I4d344d94e39f52fdd743b99390616c23c86a3a45
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 427662a..937588b 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -5263,6 +5263,7 @@
         PERMISSION_IGNORED = 12;
         SWIPE_LEFT = 13;
         SWIPE_RIGHT = 14;
+        STACK_EXPANDED = 15;
     }
     optional Action action = 6;
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index c107058..c67e3ff 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -27,7 +27,9 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.service.notification.StatusBarNotification;
 import android.util.Log;
+import android.util.StatsLog;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -54,6 +56,9 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
 /**
  * Renders bubbles in a stack and handles animating expanded and collapsed states.
  */
@@ -240,16 +245,20 @@
             // Previously expanded, notify that this bubble is no longer expanded
             notifyExpansionChanged(mExpandedBubble, false /* expanded */);
         }
+        BubbleView prevBubble = mExpandedBubble;
         mExpandedBubble = bubbleToExpand;
         if (!mIsExpanded) {
             // If we weren't previously expanded we should animate open.
             animateExpansion(true /* expand */);
+            logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
         } else {
             // Otherwise just update the views
             // TODO: probably animate / page to expanded one
             updateExpandedBubble();
             updatePointerPosition();
             requestUpdate();
+            logBubbleEvent(prevBubble, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
+            logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
         }
         mExpandedBubble.getEntry().setShowInShadeWhenBubble(false);
         notifyExpansionChanged(mExpandedBubble, true /* expanded */);
@@ -278,6 +287,7 @@
                 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
         ViewClippingUtil.setClippingDeactivated(bubbleView, true, mClippingParameters);
         requestUpdate();
+        logBubbleEvent(bubbleView, StatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
     }
 
     /**
@@ -300,6 +310,7 @@
         if (wasExpanded != mIsExpanded) {
             notifyExpansionChanged(mExpandedBubble, mIsExpanded);
         }
+        logBubbleEvent(bubbleView, StatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
     }
 
     /**
@@ -308,6 +319,8 @@
     public void stackDismissed() {
         collapseStack();
         mBubbleContainer.removeAllViews();
+        logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
+                StatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
     }
 
     /**
@@ -331,6 +344,7 @@
             entry.setShowInShadeWhenBubble(false);
             requestUpdate();
         }
+        logBubbleEvent(bubbleView, StatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
     }
 
     /**
@@ -366,6 +380,7 @@
             // TODO: Save opened bubble & move it to top of stack
             animateExpansion(false /* shouldExpand */);
             notifyExpansionChanged(mExpandedBubble, mIsExpanded);
+            logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
         }
     }
 
@@ -382,6 +397,7 @@
         if (!mIsExpanded) {
             mExpandedBubble = getTopBubble();
             setExpandedBubble(mExpandedBubble);
+            logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
         }
     }
 
@@ -574,6 +590,9 @@
                         .setStiffness(SpringForce.STIFFNESS_LOW)
                         .setDampingRatio(SPRING_DAMPING_RATIO),
                 /* destination */ null);
+
+        logBubbleEvent(null /* no bubble associated with bubble stack move */,
+                StatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
     }
 
     /**
@@ -733,4 +752,72 @@
         viewState.headsUpIsVisible = false;
         viewState.applyToView(view);
     }
+
+    /**
+     * @return the number of bubbles in the stack view.
+     */
+    private int getBubbleCount() {
+        return mBubbleContainer.getChildCount();
+    }
+
+    /**
+     * Finds the bubble index within the stack.
+     *
+     * @param bubbleView the view of the bubble.
+     * @return the index of the bubble view within the bubble stack. The range of the position
+     * is between 0 and the bubble count minus 1.
+     */
+    private int getBubbleIndex(BubbleView bubbleView) {
+        return mBubbleContainer.indexOfChild(bubbleView);
+    }
+
+    /**
+     * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
+     */
+    private float getNormalizedXPosition() {
+        return new BigDecimal(getPosition().x / mDisplaySize.x)
+                .setScale(4, RoundingMode.CEILING.HALF_UP)
+                .floatValue();
+    }
+
+    /**
+     * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
+     */
+    private float getNormalizedYPosition() {
+        return new BigDecimal(getPosition().y / mDisplaySize.y)
+                .setScale(4, RoundingMode.CEILING.HALF_UP)
+                .floatValue();
+    }
+
+    /**
+     * Logs the bubble UI event.
+     *
+     * @param bubble the bubble that is being interacted on. Null value indicates that
+     *               the user interaction is not specific to one bubble.
+     * @param action the user interaction enum.
+     */
+    private void logBubbleEvent(@Nullable BubbleView bubble, int action) {
+        if (bubble == null) {
+            StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
+                    null /* package name */,
+                    null /* notification channel */,
+                    0 /* notification ID */,
+                    0 /* bubble position */,
+                    getBubbleCount(),
+                    action,
+                    getNormalizedXPosition(),
+                    getNormalizedYPosition());
+        } else {
+            StatusBarNotification notification = bubble.getEntry().notification;
+            StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
+                    notification.getPackageName(),
+                    notification.getNotification().getChannelId(),
+                    notification.getId(),
+                    getBubbleIndex(bubble),
+                    getBubbleCount(),
+                    action,
+                    getNormalizedXPosition(),
+                    getNormalizedYPosition());
+        }
+    }
 }