1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.bubbles;
18 
19 import static android.view.Display.INVALID_DISPLAY;
20 
21 import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW;
22 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
23 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
24 
25 import android.app.ActivityOptions;
26 import android.app.ActivityTaskManager;
27 import android.app.ActivityView;
28 import android.app.PendingIntent;
29 import android.content.ComponentName;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.pm.ApplicationInfo;
33 import android.content.pm.PackageManager;
34 import android.content.res.Resources;
35 import android.content.res.TypedArray;
36 import android.graphics.Color;
37 import android.graphics.Insets;
38 import android.graphics.Point;
39 import android.graphics.Rect;
40 import android.graphics.drawable.Drawable;
41 import android.graphics.drawable.ShapeDrawable;
42 import android.os.RemoteException;
43 import android.service.notification.StatusBarNotification;
44 import android.util.AttributeSet;
45 import android.util.Log;
46 import android.util.StatsLog;
47 import android.view.View;
48 import android.view.WindowInsets;
49 import android.view.WindowManager;
50 import android.widget.LinearLayout;
51 
52 import com.android.internal.policy.ScreenDecorationsUtils;
53 import com.android.systemui.Dependency;
54 import com.android.systemui.R;
55 import com.android.systemui.recents.TriangleShape;
56 import com.android.systemui.statusbar.AlphaOptimizedButton;
57 
58 /**
59  * Container for the expanded bubble view, handles rendering the caret and settings icon.
60  */
61 public class BubbleExpandedView extends LinearLayout implements View.OnClickListener {
62     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES;
63 
64     private enum ActivityViewStatus {
65         // ActivityView is being initialized, cannot start an activity yet.
66         INITIALIZING,
67         // ActivityView is initialized, and ready to start an activity.
68         INITIALIZED,
69         // Activity runs in the ActivityView.
70         ACTIVITY_STARTED,
71         // ActivityView is released, so activity launching will no longer be permitted.
72         RELEASED,
73     }
74 
75     // The triangle pointing to the expanded view
76     private View mPointerView;
77     private int mPointerMargin;
78 
79     private AlphaOptimizedButton mSettingsIcon;
80 
81     // Views for expanded state
82     private ActivityView mActivityView;
83 
84     private ActivityViewStatus mActivityViewStatus = ActivityViewStatus.INITIALIZING;
85     private int mTaskId = -1;
86 
87     private PendingIntent mBubbleIntent;
88 
89     private boolean mKeyboardVisible;
90     private boolean mNeedsNewHeight;
91 
92     private Point mDisplaySize;
93     private int mMinHeight;
94     private int mSettingsIconHeight;
95     private int mPointerWidth;
96     private int mPointerHeight;
97     private ShapeDrawable mPointerDrawable;
98     private Rect mTempRect = new Rect();
99     private int[] mTempLoc = new int[2];
100     private int mExpandedViewTouchSlop;
101 
102     private Bubble mBubble;
103     private PackageManager mPm;
104     private String mAppName;
105     private Drawable mAppIcon;
106 
107     private BubbleController mBubbleController = Dependency.get(BubbleController.class);
108     private WindowManager mWindowManager;
109 
110     private BubbleStackView mStackView;
111 
112     private ActivityView.StateCallback mStateCallback = new ActivityView.StateCallback() {
113         @Override
114         public void onActivityViewReady(ActivityView view) {
115             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
116                 Log.d(TAG, "onActivityViewReady: mActivityViewStatus=" + mActivityViewStatus
117                         + " bubble=" + getBubbleKey());
118             }
119             switch (mActivityViewStatus) {
120                 case INITIALIZING:
121                 case INITIALIZED:
122                     // Custom options so there is no activity transition animation
123                     ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
124                             0 /* enterResId */, 0 /* exitResId */);
125                     // Post to keep the lifecycle normal
126                     post(() -> {
127                         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
128                             Log.d(TAG, "onActivityViewReady: calling startActivity, "
129                                     + "bubble=" + getBubbleKey());
130                         }
131                         try {
132                             mActivityView.startActivity(mBubbleIntent, options);
133                         } catch (RuntimeException e) {
134                             // If there's a runtime exception here then there's something
135                             // wrong with the intent, we can't really recover / try to populate
136                             // the bubble again so we'll just remove it.
137                             Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
138                                     + ", " + e.getMessage() + "; removing bubble");
139                             mBubbleController.removeBubble(mBubble.getKey(),
140                                     BubbleController.DISMISS_INVALID_INTENT);
141                         }
142                     });
143                     mActivityViewStatus = ActivityViewStatus.ACTIVITY_STARTED;
144             }
145         }
146 
147         @Override
148         public void onActivityViewDestroyed(ActivityView view) {
149             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
150                 Log.d(TAG, "onActivityViewDestroyed: mActivityViewStatus=" + mActivityViewStatus
151                         + " bubble=" + getBubbleKey());
152             }
153             mActivityViewStatus = ActivityViewStatus.RELEASED;
154         }
155 
156         @Override
157         public void onTaskCreated(int taskId, ComponentName componentName) {
158             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
159                 Log.d(TAG, "onTaskCreated: taskId=" + taskId
160                         + " bubble=" + getBubbleKey());
161             }
162             // Since Bubble ActivityView applies singleTaskDisplay this is
163             // guaranteed to only be called once per ActivityView. The taskId is
164             // saved to use for removeTask, preventing appearance in recent tasks.
165             mTaskId = taskId;
166         }
167 
168         /**
169          * This is only called for tasks on this ActivityView, which is also set to
170          * single-task mode -- meaning never more than one task on this display. If a task
171          * is being removed, it's the top Activity finishing and this bubble should
172          * be removed or collapsed.
173          */
174         @Override
175         public void onTaskRemovalStarted(int taskId) {
176             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
177                 Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId
178                         + " mActivityViewStatus=" + mActivityViewStatus
179                         + " bubble=" + getBubbleKey());
180             }
181             if (mBubble != null) {
182                 // Must post because this is called from a binder thread.
183                 post(() -> mBubbleController.removeBubble(mBubble.getKey(),
184                         BubbleController.DISMISS_TASK_FINISHED));
185             }
186         }
187     };
188 
BubbleExpandedView(Context context)189     public BubbleExpandedView(Context context) {
190         this(context, null);
191     }
192 
BubbleExpandedView(Context context, AttributeSet attrs)193     public BubbleExpandedView(Context context, AttributeSet attrs) {
194         this(context, attrs, 0);
195     }
196 
BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr)197     public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
198         this(context, attrs, defStyleAttr, 0);
199     }
200 
BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)201     public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
202             int defStyleRes) {
203         super(context, attrs, defStyleAttr, defStyleRes);
204         mPm = context.getPackageManager();
205         mDisplaySize = new Point();
206         mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
207         // Get the real size -- this includes screen decorations (notches, statusbar, navbar).
208         mWindowManager.getDefaultDisplay().getRealSize(mDisplaySize);
209         Resources res = getResources();
210         mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height);
211         mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
212         mExpandedViewTouchSlop = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_slop);
213     }
214 
215     @Override
onFinishInflate()216     protected void onFinishInflate() {
217         super.onFinishInflate();
218         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
219             Log.d(TAG, "onFinishInflate: bubble=" + getBubbleKey());
220         }
221 
222         Resources res = getResources();
223         mPointerView = findViewById(R.id.pointer_view);
224         mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
225         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
226 
227 
228         mPointerDrawable = new ShapeDrawable(TriangleShape.create(
229                 mPointerWidth, mPointerHeight, true /* pointUp */));
230         mPointerView.setBackground(mPointerDrawable);
231         mPointerView.setVisibility(INVISIBLE);
232 
233         mSettingsIconHeight = getContext().getResources().getDimensionPixelSize(
234                 R.dimen.bubble_settings_size);
235         mSettingsIcon = findViewById(R.id.settings_button);
236         mSettingsIcon.setOnClickListener(this);
237 
238         mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */,
239                 true /* singleTaskInstance */);
240         // Set ActivityView's alpha value as zero, since there is no view content to be shown.
241         setContentVisibility(false);
242         addView(mActivityView);
243 
244         // Expanded stack layout, top to bottom:
245         // Expanded view container
246         // ==> bubble row
247         // ==> expanded view
248         //   ==> activity view
249         //   ==> manage button
250         bringChildToFront(mActivityView);
251         bringChildToFront(mSettingsIcon);
252 
253         applyThemeAttrs();
254 
255         setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
256             // Keep track of IME displaying because we should not make any adjustments that might
257             // cause a config change while the IME is displayed otherwise it'll loose focus.
258             final int keyboardHeight = insets.getSystemWindowInsetBottom()
259                     - insets.getStableInsetBottom();
260             mKeyboardVisible = keyboardHeight != 0;
261             if (!mKeyboardVisible && mNeedsNewHeight) {
262                 updateHeight();
263             }
264             return view.onApplyWindowInsets(insets);
265         });
266     }
267 
getBubbleKey()268     private String getBubbleKey() {
269         return mBubble != null ? mBubble.getKey() : "null";
270     }
271 
applyThemeAttrs()272     void applyThemeAttrs() {
273         TypedArray ta = getContext().obtainStyledAttributes(R.styleable.BubbleExpandedView);
274         int bgColor = ta.getColor(
275                 R.styleable.BubbleExpandedView_android_colorBackgroundFloating, Color.WHITE);
276         float cornerRadius = ta.getDimension(
277                 R.styleable.BubbleExpandedView_android_dialogCornerRadius, 0);
278         ta.recycle();
279 
280         // Update triangle color.
281         mPointerDrawable.setTint(bgColor);
282 
283         // Update ActivityView cornerRadius
284         if (ScreenDecorationsUtils.supportsRoundedCornersOnWindows(mContext.getResources())) {
285             mActivityView.setCornerRadius(cornerRadius);
286         }
287     }
288 
289     @Override
onDetachedFromWindow()290     protected void onDetachedFromWindow() {
291         super.onDetachedFromWindow();
292         mKeyboardVisible = false;
293         mNeedsNewHeight = false;
294         if (mActivityView != null) {
295             mActivityView.setForwardedInsets(Insets.of(0, 0, 0, 0));
296         }
297         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
298             Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey());
299         }
300     }
301 
302     /**
303      * Set visibility of contents in the expanded state.
304      *
305      * @param visibility {@code true} if the contents should be visible on the screen.
306      *
307      * Note that this contents visibility doesn't affect visibility at {@link android.view.View},
308      * and setting {@code false} actually means rendering the contents in transparent.
309      */
setContentVisibility(boolean visibility)310     void setContentVisibility(boolean visibility) {
311         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
312             Log.d(TAG, "setContentVisibility: visibility=" + visibility
313                     + " bubble=" + getBubbleKey());
314         }
315         final float alpha = visibility ? 1f : 0f;
316         mPointerView.setAlpha(alpha);
317         if (mActivityView != null) {
318             mActivityView.setAlpha(alpha);
319         }
320     }
321 
322     /**
323      * Called by {@link BubbleStackView} when the insets for the expanded state should be updated.
324      * This should be done post-move and post-animation.
325      */
updateInsets(WindowInsets insets)326     void updateInsets(WindowInsets insets) {
327         if (usingActivityView()) {
328             int[] screenLoc = mActivityView.getLocationOnScreen();
329             final int activityViewBottom = screenLoc[1] + mActivityView.getHeight();
330             final int keyboardTop = mDisplaySize.y - Math.max(insets.getSystemWindowInsetBottom(),
331                     insets.getDisplayCutout() != null
332                             ? insets.getDisplayCutout().getSafeInsetBottom()
333                             : 0);
334             final int insetsBottom = Math.max(activityViewBottom - keyboardTop, 0);
335             mActivityView.setForwardedInsets(Insets.of(0, 0, 0, insetsBottom));
336         }
337     }
338 
339     /**
340      * Sets the bubble used to populate this view.
341      */
setBubble(Bubble bubble, BubbleStackView stackView, String appName)342     public void setBubble(Bubble bubble, BubbleStackView stackView, String appName) {
343         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
344             Log.d(TAG, "setBubble: bubble=" + (bubble != null ? bubble.getKey() : "null"));
345         }
346 
347         mStackView = stackView;
348         mBubble = bubble;
349         mAppName = appName;
350 
351         try {
352             ApplicationInfo info = mPm.getApplicationInfo(
353                     bubble.getPackageName(),
354                     PackageManager.MATCH_UNINSTALLED_PACKAGES
355                             | PackageManager.MATCH_DISABLED_COMPONENTS
356                             | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
357                             | PackageManager.MATCH_DIRECT_BOOT_AWARE);
358             mAppIcon = mPm.getApplicationIcon(info);
359         } catch (PackageManager.NameNotFoundException e) {
360             // Do nothing.
361         }
362         if (mAppIcon == null) {
363             mAppIcon = mPm.getDefaultActivityIcon();
364         }
365         applyThemeAttrs();
366         showSettingsIcon();
367         updateExpandedView();
368     }
369 
370     /**
371      * Lets activity view know it should be shown / populated.
372      */
populateExpandedView()373     public void populateExpandedView() {
374         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
375             Log.d(TAG, "populateExpandedView: "
376                     + "bubble=" + getBubbleKey());
377         }
378 
379         if (usingActivityView()) {
380             mActivityView.setCallback(mStateCallback);
381         } else {
382             Log.e(TAG, "Cannot populate expanded view.");
383         }
384     }
385 
386     /**
387      * Updates the bubble backing this view. This will not re-populate ActivityView, it will
388      * only update the deep-links in the title, and the height of the view.
389      */
update(Bubble bubble)390     public void update(Bubble bubble) {
391         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
392             Log.d(TAG, "update: bubble=" + (bubble != null ? bubble.getKey() : "null"));
393         }
394         if (bubble.getKey().equals(mBubble.getKey())) {
395             mBubble = bubble;
396             updateSettingsContentDescription();
397             updateHeight();
398         } else {
399             Log.w(TAG, "Trying to update entry with different key, new bubble: "
400                     + bubble.getKey() + " old bubble: " + bubble.getKey());
401         }
402     }
403 
updateExpandedView()404     private void updateExpandedView() {
405         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
406             Log.d(TAG, "updateExpandedView: bubble="
407                     + getBubbleKey());
408         }
409 
410         mBubbleIntent = mBubble.getBubbleIntent(mContext);
411         if (mBubbleIntent != null) {
412             setContentVisibility(false);
413             mActivityView.setVisibility(VISIBLE);
414         }
415         updateView();
416     }
417 
performBackPressIfNeeded()418     boolean performBackPressIfNeeded() {
419         if (!usingActivityView()) {
420             return false;
421         }
422         mActivityView.performBackPress();
423         return true;
424     }
425 
updateHeight()426     void updateHeight() {
427         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
428             Log.d(TAG, "updateHeight: bubble=" + getBubbleKey());
429         }
430         if (usingActivityView()) {
431             float desiredHeight = Math.max(mBubble.getDesiredHeight(mContext), mMinHeight);
432             float height = Math.min(desiredHeight, getMaxExpandedHeight());
433             height = Math.max(height, mMinHeight);
434             LayoutParams lp = (LayoutParams) mActivityView.getLayoutParams();
435             mNeedsNewHeight =  lp.height != height;
436             if (!mKeyboardVisible) {
437                 // If the keyboard is visible... don't adjust the height because that will cause
438                 // a configuration change and the keyboard will be lost.
439                 lp.height = (int) height;
440                 mActivityView.setLayoutParams(lp);
441                 mNeedsNewHeight = false;
442             }
443             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
444                 Log.d(TAG, "updateHeight: bubble=" + getBubbleKey() + " height=" + height
445                         + " mNeedsNewHeight=" + mNeedsNewHeight);
446             }
447         }
448     }
449 
getMaxExpandedHeight()450     private int getMaxExpandedHeight() {
451         mWindowManager.getDefaultDisplay().getRealSize(mDisplaySize);
452         int[] windowLocation = mActivityView.getLocationOnScreen();
453         int bottomInset = getRootWindowInsets() != null
454                 ? getRootWindowInsets().getStableInsetBottom()
455                 : 0;
456         return mDisplaySize.y - windowLocation[1] - mSettingsIconHeight - mPointerHeight
457                 - mPointerMargin - bottomInset;
458     }
459 
460     /**
461      * Whether the provided x, y values (in raw coordinates) are in a touchable area of the
462      * expanded view.
463      *
464      * The touchable areas are the ActivityView (plus some slop around it) and the manage button.
465      */
intersectingTouchableContent(int rawX, int rawY)466     boolean intersectingTouchableContent(int rawX, int rawY) {
467         mTempRect.setEmpty();
468         if (mActivityView != null) {
469             mTempLoc = mActivityView.getLocationOnScreen();
470             mTempRect.set(mTempLoc[0] - mExpandedViewTouchSlop,
471                     mTempLoc[1] - mExpandedViewTouchSlop,
472                     mTempLoc[0] + mActivityView.getWidth() + mExpandedViewTouchSlop,
473                     mTempLoc[1] + mActivityView.getHeight() + mExpandedViewTouchSlop);
474         }
475         if (mTempRect.contains(rawX, rawY)) {
476             return true;
477         }
478         mTempLoc = mSettingsIcon.getLocationOnScreen();
479         mTempRect.set(mTempLoc[0],
480                 mTempLoc[1],
481                 mTempLoc[0] + mSettingsIcon.getWidth(),
482                 mTempLoc[1] + mSettingsIcon.getHeight());
483         if (mTempRect.contains(rawX, rawY)) {
484             return true;
485         }
486         return false;
487     }
488 
489     @Override
onClick(View view)490     public void onClick(View view) {
491         if (mBubble == null) {
492             return;
493         }
494         int id = view.getId();
495         if (id == R.id.settings_button) {
496             Intent intent = mBubble.getSettingsIntent();
497             mStackView.collapseStack(() -> {
498                 mContext.startActivityAsUser(intent, mBubble.getEntry().notification.getUser());
499                 logBubbleClickEvent(mBubble,
500                         StatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
501             });
502         }
503     }
504 
updateSettingsContentDescription()505     private void updateSettingsContentDescription() {
506         mSettingsIcon.setContentDescription(getResources().getString(
507                 R.string.bubbles_settings_button_description, mAppName));
508     }
509 
showSettingsIcon()510     void showSettingsIcon() {
511         updateSettingsContentDescription();
512         mSettingsIcon.setVisibility(VISIBLE);
513     }
514 
515     /**
516      * Update appearance of the expanded view being displayed.
517      */
updateView()518     public void updateView() {
519         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
520             Log.d(TAG, "updateView: bubble="
521                     + getBubbleKey());
522         }
523         if (usingActivityView()
524                 && mActivityView.getVisibility() == VISIBLE
525                 && mActivityView.isAttachedToWindow()) {
526             mActivityView.onLocationChanged();
527         }
528         updateHeight();
529     }
530 
531     /**
532      * Set the x position that the tip of the triangle should point to.
533      */
setPointerPosition(float x)534     public void setPointerPosition(float x) {
535         float halfPointerWidth = mPointerWidth / 2f;
536         float pointerLeft = x - halfPointerWidth;
537         mPointerView.setTranslationX(pointerLeft);
538         mPointerView.setVisibility(VISIBLE);
539     }
540 
541     /**
542      * Removes and releases an ActivityView if one was previously created for this bubble.
543      */
cleanUpExpandedState()544     public void cleanUpExpandedState() {
545         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
546             Log.d(TAG, "cleanUpExpandedState: mActivityViewStatus=" + mActivityViewStatus
547                     + ", bubble=" + getBubbleKey());
548         }
549         if (mActivityView == null) {
550             return;
551         }
552         switch (mActivityViewStatus) {
553             case INITIALIZED:
554             case ACTIVITY_STARTED:
555                 mActivityView.release();
556         }
557         if (mTaskId != -1) {
558             try {
559                 ActivityTaskManager.getService().removeTask(mTaskId);
560             } catch (RemoteException e) {
561                 Log.w(TAG, "Failed to remove taskId " + mTaskId);
562             }
563             mTaskId = -1;
564         }
565         removeView(mActivityView);
566 
567         mActivityView = null;
568     }
569 
570     /**
571      * Called when the last task is removed from a {@link android.hardware.display.VirtualDisplay}
572      * which {@link ActivityView} uses.
573      */
notifyDisplayEmpty()574     void notifyDisplayEmpty() {
575         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
576             Log.d(TAG, "notifyDisplayEmpty: bubble="
577                     + getBubbleKey()
578                     + " mActivityViewStatus=" + mActivityViewStatus);
579         }
580         if (mActivityViewStatus == ActivityViewStatus.ACTIVITY_STARTED) {
581             mActivityViewStatus = ActivityViewStatus.INITIALIZED;
582         }
583     }
584 
usingActivityView()585     private boolean usingActivityView() {
586         return mBubbleIntent != null && mActivityView != null;
587     }
588 
589     /**
590      * @return the display id of the virtual display.
591      */
getVirtualDisplayId()592     public int getVirtualDisplayId() {
593         if (usingActivityView()) {
594             return mActivityView.getVirtualDisplayId();
595         }
596         return INVALID_DISPLAY;
597     }
598 
599     /**
600      * Logs bubble UI click event.
601      *
602      * @param bubble the bubble notification entry that user is interacting with.
603      * @param action the user interaction enum.
604      */
logBubbleClickEvent(Bubble bubble, int action)605     private void logBubbleClickEvent(Bubble bubble, int action) {
606         StatusBarNotification notification = bubble.getEntry().notification;
607         StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
608                 notification.getPackageName(),
609                 notification.getNotification().getChannelId(),
610                 notification.getId(),
611                 mStackView.getBubbleIndex(mStackView.getExpandedBubble()),
612                 mStackView.getBubbleCount(),
613                 action,
614                 mStackView.getNormalizedXPosition(),
615                 mStackView.getNormalizedYPosition(),
616                 bubble.showInShadeWhenBubble(),
617                 bubble.isOngoing(),
618                 false /* isAppForeground (unused) */);
619     }
620 }
621