1 /*
2  * Copyright (C) 2023 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 package com.android.launcher3.taskbar.bubbles;
17 
18 import static android.view.View.INVISIBLE;
19 import static android.view.View.VISIBLE;
20 
21 import android.content.res.Resources;
22 import android.graphics.Point;
23 import android.graphics.PointF;
24 import android.graphics.Rect;
25 import android.util.DisplayMetrics;
26 import android.util.Log;
27 import android.util.TypedValue;
28 import android.view.Gravity;
29 import android.view.MotionEvent;
30 import android.view.View;
31 import android.widget.FrameLayout;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 
36 import com.android.launcher3.R;
37 import com.android.launcher3.anim.AnimatedFloat;
38 import com.android.launcher3.taskbar.TaskbarActivityContext;
39 import com.android.launcher3.taskbar.TaskbarControllers;
40 import com.android.launcher3.taskbar.TaskbarInsetsController;
41 import com.android.launcher3.taskbar.TaskbarStashController;
42 import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator;
43 import com.android.launcher3.util.MultiPropertyFactory;
44 import com.android.launcher3.util.MultiValueAlpha;
45 import com.android.quickstep.SystemUiProxy;
46 import com.android.wm.shell.common.bubbles.BubbleBarLocation;
47 
48 import java.util.List;
49 import java.util.Objects;
50 import java.util.function.Consumer;
51 
52 /**
53  * Controller for {@link BubbleBarView}. Manages the visibility of the bubble bar as well as
54  * responding to changes in bubble state provided by BubbleBarController.
55  */
56 public class BubbleBarViewController {
57 
58     private static final String TAG = "BubbleBarViewController";
59     private static final float APP_ICON_SMALL_DP = 44f;
60     private static final float APP_ICON_MEDIUM_DP = 48f;
61     private static final float APP_ICON_LARGE_DP = 52f;
62     private final SystemUiProxy mSystemUiProxy;
63     private final TaskbarActivityContext mActivity;
64     private final BubbleBarView mBarView;
65     private int mIconSize;
66 
67     // Initialized in init.
68     private BubbleStashController mBubbleStashController;
69     private BubbleBarController mBubbleBarController;
70     private BubbleDragController mBubbleDragController;
71     private TaskbarStashController mTaskbarStashController;
72     private TaskbarInsetsController mTaskbarInsetsController;
73     private View.OnClickListener mBubbleClickListener;
74     private View.OnClickListener mBubbleBarClickListener;
75 
76     // These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing
77     private final MultiValueAlpha mBubbleBarAlpha;
78     private final AnimatedFloat mBubbleBarScale = new AnimatedFloat(this::updateScale);
79     private final AnimatedFloat mBubbleBarTranslationY = new AnimatedFloat(
80             this::updateTranslationY);
81 
82     // Modified when swipe up is happening on the bubble bar or task bar.
83     private float mBubbleBarSwipeUpTranslationY;
84 
85     // Whether the bar is hidden for a sysui state.
86     private boolean mHiddenForSysui;
87     // Whether the bar is hidden because there are no bubbles.
88     private boolean mHiddenForNoBubbles = true;
89     private boolean mShouldShowEducation;
90 
91     private BubbleBarViewAnimator mBubbleBarViewAnimator;
92 
93     @Nullable
94     private BubbleBarBoundsChangeListener mBoundsChangeListener;
95 
BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView)96     public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView) {
97         mActivity = activity;
98         mBarView = barView;
99         mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity);
100         mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
101         mIconSize = activity.getResources().getDimensionPixelSize(
102                 R.dimen.bubblebar_icon_size);
103     }
104 
init(TaskbarControllers controllers, BubbleControllers bubbleControllers)105     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
106         mBubbleStashController = bubbleControllers.bubbleStashController;
107         mBubbleBarController = bubbleControllers.bubbleBarController;
108         mBubbleDragController = bubbleControllers.bubbleDragController;
109         mTaskbarStashController = controllers.taskbarStashController;
110         mTaskbarInsetsController = controllers.taskbarInsetsController;
111         mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController);
112 
113         mActivity.addOnDeviceProfileChangeListener(
114                 dp -> updateBubbleBarIconSize(dp.taskbarIconSize, /* animate= */ true));
115         updateBubbleBarIconSize(mActivity.getDeviceProfile().taskbarIconSize, /* animate= */ false);
116         mBubbleBarScale.updateValue(1f);
117         mBubbleClickListener = v -> onBubbleClicked(v);
118         mBubbleBarClickListener = v -> onBubbleBarClicked();
119         mBubbleDragController.setupBubbleBarView(mBarView);
120         mBarView.setOnClickListener(mBubbleBarClickListener);
121         mBarView.addOnLayoutChangeListener(
122                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
123                     mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
124                     if (mBoundsChangeListener != null) {
125                         mBoundsChangeListener.onBoundsChanged();
126                     }
127                 });
128         mBarView.setController(new BubbleBarView.Controller() {
129             @Override
130             public float getBubbleBarTranslationY() {
131                 return mBubbleStashController.getBubbleBarTranslationY();
132             }
133 
134             @Override
135             public void onBubbleBarTouchedWhileAnimating() {
136                 BubbleBarViewController.this.onBubbleBarTouchedWhileAnimating();
137             }
138         });
139     }
140 
onBubbleClicked(View v)141     private void onBubbleClicked(View v) {
142         BubbleBarItem bubble = ((BubbleView) v).getBubble();
143         if (bubble == null) {
144             Log.e(TAG, "bubble click listener, bubble was null");
145         }
146 
147         final String currentlySelected = mBubbleBarController.getSelectedBubbleKey();
148         if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) {
149             // Tapping the currently selected bubble while expanded collapses the view.
150             setExpanded(false);
151             mBubbleStashController.stashBubbleBar();
152         } else {
153             mBubbleBarController.showAndSelectBubble(bubble);
154         }
155     }
156 
onBubbleBarTouchedWhileAnimating()157     private void onBubbleBarTouchedWhileAnimating() {
158         mBubbleBarViewAnimator.onBubbleBarTouchedWhileAnimating();
159         mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY());
160     }
161 
onBubbleBarClicked()162     private void onBubbleBarClicked() {
163         if (mShouldShowEducation) {
164             mShouldShowEducation = false;
165             // Get the bubble bar bounds on screen
166             Rect bounds = new Rect();
167             mBarView.getBoundsOnScreen(bounds);
168             // Calculate user education reference position in Screen coordinates
169             Point position = new Point(bounds.centerX(), bounds.top);
170             // Show user education relative to the reference point
171             mSystemUiProxy.showUserEducation(position);
172         } else {
173             // ensure that the bubble bar has the correct translation. we may have just interrupted
174             // the animation by touching the bubble bar.
175             mBubbleBarTranslationY.animateToValue(mBubbleStashController.getBubbleBarTranslationY())
176                     .start();
177             setExpanded(true);
178         }
179     }
180 
181     /** Notifies that the stash state is changing. */
onStashStateChanging()182     public void onStashStateChanging() {
183         if (isAnimatingNewBubble()) {
184             mBubbleBarViewAnimator.onStashStateChangingWhileAnimating();
185         }
186     }
187 
188     //
189     // The below animators are exposed to BubbleStashController so it can manage the stashing
190     // animation.
191     //
192 
getBubbleBarAlpha()193     public MultiPropertyFactory<View> getBubbleBarAlpha() {
194         return mBubbleBarAlpha;
195     }
196 
getBubbleBarScale()197     public AnimatedFloat getBubbleBarScale() {
198         return mBubbleBarScale;
199     }
200 
getBubbleBarTranslationY()201     public AnimatedFloat getBubbleBarTranslationY() {
202         return mBubbleBarTranslationY;
203     }
204 
getBubbleBarCollapsedHeight()205     float getBubbleBarCollapsedHeight() {
206         return mBarView.getBubbleBarCollapsedHeight();
207     }
208 
209     /**
210      * Whether the bubble bar is visible or not.
211      */
isBubbleBarVisible()212     public boolean isBubbleBarVisible() {
213         return mBarView.getVisibility() == VISIBLE;
214     }
215 
216     /** Whether the bubble bar has bubbles. */
hasBubbles()217     public boolean hasBubbles() {
218         return mBubbleBarController.getSelectedBubbleKey() != null;
219     }
220 
221     /**
222      * @return current {@link BubbleBarLocation}
223      */
getBubbleBarLocation()224     public BubbleBarLocation getBubbleBarLocation() {
225         return mBarView.getBubbleBarLocation();
226     }
227 
228     /**
229      * Update bar {@link BubbleBarLocation}
230      */
setBubbleBarLocation(BubbleBarLocation bubbleBarLocation)231     public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
232         mBarView.setBubbleBarLocation(bubbleBarLocation);
233     }
234 
235     /**
236      * Animate bubble bar to the given location. The location change is transient. It does not
237      * update the state of the bubble bar.
238      * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}.
239      */
animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation)240     public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
241         mBarView.animateToBubbleBarLocation(bubbleBarLocation);
242     }
243 
244     /**
245      * The bounds of the bubble bar.
246      */
getBubbleBarBounds()247     public Rect getBubbleBarBounds() {
248         return mBarView.getBubbleBarBounds();
249     }
250 
251     /** Whether a new bubble is animating. */
isAnimatingNewBubble()252     public boolean isAnimatingNewBubble() {
253         return mBarView.isAnimatingNewBubble();
254     }
255 
256     /** The horizontal margin of the bubble bar from the edge of the screen. */
getHorizontalMargin()257     public int getHorizontalMargin() {
258         return mBarView.getHorizontalMargin();
259     }
260 
261     /**
262      * When the bubble bar is not stashed, it can be collapsed (the icons are in a stack) or
263      * expanded (the icons are in a row). This indicates whether the bubble bar is expanded.
264      */
isExpanded()265     public boolean isExpanded() {
266         return mBarView.isExpanded();
267     }
268 
269     /**
270      * Whether the motion event is within the bounds of the bubble bar.
271      */
isEventOverAnyItem(MotionEvent ev)272     public boolean isEventOverAnyItem(MotionEvent ev) {
273         return mBarView.isEventOverAnyItem(ev);
274     }
275 
276     //
277     // Visibility of the bubble bar
278     //
279 
280     /**
281      * Returns whether the bubble bar is hidden because there are no bubbles.
282      */
isHiddenForNoBubbles()283     public boolean isHiddenForNoBubbles() {
284         return mHiddenForNoBubbles;
285     }
286 
287     /**
288      * Sets whether the bubble bar should be hidden because there are no bubbles.
289      */
setHiddenForBubbles(boolean hidden)290     public void setHiddenForBubbles(boolean hidden) {
291         if (mHiddenForNoBubbles != hidden) {
292             mHiddenForNoBubbles = hidden;
293             updateVisibilityForStateChange();
294             if (hidden) {
295                 mBarView.setAlpha(0);
296                 mBarView.setExpanded(false);
297             }
298             mActivity.bubbleBarVisibilityChanged(!hidden);
299         }
300     }
301 
302     /** Sets a callback that updates the selected bubble after the bubble bar collapses. */
setUpdateSelectedBubbleAfterCollapse( Consumer<String> updateSelectedBubbleAfterCollapse)303     public void setUpdateSelectedBubbleAfterCollapse(
304             Consumer<String> updateSelectedBubbleAfterCollapse) {
305         mBarView.setUpdateSelectedBubbleAfterCollapse(updateSelectedBubbleAfterCollapse);
306     }
307 
308     /** Returns whether the bubble bar should be hidden because of the current sysui state. */
isHiddenForSysui()309     boolean isHiddenForSysui() {
310         return mHiddenForSysui;
311     }
312 
313     /**
314      * Sets whether the bubble bar should be hidden due to SysUI state (e.g. on lockscreen).
315      */
setHiddenForSysui(boolean hidden)316     public void setHiddenForSysui(boolean hidden) {
317         if (mHiddenForSysui != hidden) {
318             mHiddenForSysui = hidden;
319             updateVisibilityForStateChange();
320         }
321     }
322 
323     // TODO: (b/273592694) animate it
updateVisibilityForStateChange()324     private void updateVisibilityForStateChange() {
325         if (!mHiddenForSysui && !mHiddenForNoBubbles) {
326             mBarView.setVisibility(VISIBLE);
327         } else {
328             mBarView.setVisibility(INVISIBLE);
329         }
330     }
331 
332     //
333     // Modifying view related properties.
334     //
335 
updateBubbleBarIconSize(int newIconSize, boolean animate)336     private void updateBubbleBarIconSize(int newIconSize, boolean animate) {
337         Resources res = mActivity.getResources();
338         DisplayMetrics dm = res.getDisplayMetrics();
339         float smallIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
340                 APP_ICON_SMALL_DP, dm);
341         float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
342                 APP_ICON_MEDIUM_DP, dm);
343         float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
344                 APP_ICON_LARGE_DP, dm);
345         float smallMediumThreshold = (smallIconSize + mediumIconSize) / 2f;
346         float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f;
347         mIconSize = newIconSize <= smallMediumThreshold
348                 ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_small) :
349                 res.getDimensionPixelSize(R.dimen.bubblebar_icon_size);
350         float bubbleBarPadding = newIconSize >= mediumLargeThreshold
351                 ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_large) :
352                 res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
353         if (animate) {
354             mBarView.animateBubbleBarIconSize(mIconSize, bubbleBarPadding);
355         } else {
356             mBarView.setIconSizeAndPadding(mIconSize, bubbleBarPadding);
357         }
358     }
359 
360     /**
361      * Sets the translation of the bubble bar during the swipe up gesture.
362      */
setTranslationYForSwipe(float transY)363     public void setTranslationYForSwipe(float transY) {
364         mBubbleBarSwipeUpTranslationY = transY;
365         updateTranslationY();
366     }
367 
updateTranslationY()368     private void updateTranslationY() {
369         mBarView.setTranslationY(mBubbleBarTranslationY.value
370                 + mBubbleBarSwipeUpTranslationY);
371     }
372 
373     /**
374      * Applies scale properties for the entire bubble bar.
375      */
updateScale()376     private void updateScale() {
377         float scale = mBubbleBarScale.value;
378         mBarView.setScaleX(scale);
379         mBarView.setScaleY(scale);
380     }
381 
382     //
383     // Manipulating the specific bubble views in the bar
384     //
385 
386     /**
387      * Removes the provided bubble from the bubble bar.
388      */
removeBubble(BubbleBarItem b)389     public void removeBubble(BubbleBarItem b) {
390         if (b != null) {
391             mBarView.removeView(b.getView());
392         } else {
393             Log.w(TAG, "removeBubble, bubble was null!");
394         }
395     }
396 
397     /**
398      * Adds the provided bubble to the bubble bar.
399      */
addBubble(BubbleBarItem b, boolean isExpanding, boolean suppressAnimation)400     public void addBubble(BubbleBarItem b, boolean isExpanding, boolean suppressAnimation) {
401         if (b != null) {
402             mBarView.addBubble(
403                     b.getView(), new FrameLayout.LayoutParams(mIconSize, mIconSize, Gravity.LEFT));
404             b.getView().setOnClickListener(mBubbleClickListener);
405             mBubbleDragController.setupBubbleView(b.getView());
406 
407             if (b instanceof BubbleBarOverflow) {
408                 return;
409             }
410 
411             if (suppressAnimation || !(b instanceof BubbleBarBubble bubble)) {
412                 // the bubble bar and handle are initialized as part of the first bubble animation.
413                 // if the animation is suppressed, immediately stash or show the bubble bar to
414                 // ensure they've been initialized.
415                 if (mTaskbarStashController.isInApp()) {
416                     mBubbleStashController.stashBubbleBarImmediate();
417                 } else {
418                     mBubbleStashController.showBubbleBarImmediate();
419                 }
420                 return;
421             }
422             animateBubbleNotification(bubble, isExpanding);
423         } else {
424             Log.w(TAG, "addBubble, bubble was null!");
425         }
426     }
427 
428     /** Animates the bubble bar to notify the user about a bubble change. */
animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding)429     public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding) {
430         boolean isInApp = mTaskbarStashController.isInApp();
431         // if this is the first bubble, animate to the initial state. one bubble is the overflow
432         // so check for at most 2 children.
433         if (mBarView.getChildCount() <= 2) {
434             mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
435             return;
436         }
437 
438         // only animate the new bubble if we're in an app and not auto expanding
439         if (isInApp && !isExpanding && !isExpanded()) {
440             mBubbleBarViewAnimator.animateBubbleInForStashed(bubble);
441         }
442     }
443 
444     /**
445      * Reorders the bubbles based on the provided list.
446      */
reorderBubbles(List<BubbleBarBubble> newOrder)447     public void reorderBubbles(List<BubbleBarBubble> newOrder) {
448         List<BubbleView> viewList = newOrder.stream().filter(Objects::nonNull)
449                 .map(BubbleBarBubble::getView).toList();
450         mBarView.reorder(viewList);
451     }
452 
453     /**
454      * Updates the selected bubble.
455      */
updateSelectedBubble(BubbleBarItem newlySelected)456     public void updateSelectedBubble(BubbleBarItem newlySelected) {
457         mBarView.setSelectedBubble(newlySelected.getView());
458     }
459 
460     /**
461      * Sets whether the bubble bar should be expanded (not unstashed, but have the contents
462      * within it expanded). This method notifies SystemUI that the bubble bar is expanded and
463      * showing a selected bubble. This method should ONLY be called from UI events originating
464      * from Launcher.
465      */
setExpanded(boolean isExpanded)466     public void setExpanded(boolean isExpanded) {
467         if (isExpanded != mBarView.isExpanded()) {
468             mBarView.setExpanded(isExpanded);
469             if (!isExpanded) {
470                 mSystemUiProxy.collapseBubbles();
471             } else {
472                 mBubbleBarController.showSelectedBubble();
473                 mTaskbarStashController.updateAndAnimateTransientTaskbar(true /* stash */,
474                         false /* shouldBubblesFollow */);
475             }
476         }
477     }
478 
479     /**
480      * Sets whether the bubble bar should be expanded. This method is used in response to UI events
481      * from SystemUI.
482      */
setExpandedFromSysui(boolean isExpanded)483     public void setExpandedFromSysui(boolean isExpanded) {
484         if (!isExpanded) {
485             mBubbleStashController.stashBubbleBar();
486         } else {
487             mBubbleStashController.showBubbleBar(true /* expand the bubbles */);
488         }
489     }
490 
491     /** Marks as should show education and shows the bubble bar in a collapsed state */
prepareToShowEducation()492     public void prepareToShowEducation() {
493         mShouldShowEducation = true;
494         mBubbleStashController.showBubbleBar(false /* expand the bubbles */);
495     }
496 
497     /**
498      * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI
499      * that a bubble is being dragged to dismiss.
500      * @param bubbleView dragged bubble view
501      */
onBubbleDragStart(@onNull BubbleView bubbleView)502     public void onBubbleDragStart(@NonNull BubbleView bubbleView) {
503         if (bubbleView.getBubble() == null) return;
504 
505         mSystemUiProxy.startBubbleDrag(bubbleView.getBubble().getKey());
506         mBarView.setDraggedBubble(bubbleView);
507     }
508 
509     /**
510      * Notifies SystemUI to expand the selected bubble when the bubble is released.
511      */
onBubbleDragRelease(BubbleBarLocation location)512     public void onBubbleDragRelease(BubbleBarLocation location) {
513         mSystemUiProxy.stopBubbleDrag(location, mBarView.getRestingTopPositionOnScreen());
514     }
515 
516     /**
517      * Notifies {@link BubbleBarView} that drag and all animations are finished.
518      */
onBubbleDragEnd()519     public void onBubbleDragEnd() {
520         mBarView.setDraggedBubble(null);
521     }
522 
523     /** Notifies that dragging the bubble bar ended. */
onBubbleBarDragEnd()524     public void onBubbleBarDragEnd() {
525         // we may have changed the bubble bar translation Y value from the value it had at the
526         // beginning of the drag, so update the translation Y animator state
527         mBubbleBarTranslationY.updateValue(mBarView.getTranslationY());
528     }
529 
530     /**
531      * Get translation for bubble bar when drag is released.
532      *
533      * @see BubbleBarView#getBubbleBarDragReleaseTranslation(PointF, BubbleBarLocation)
534      */
getBubbleBarDragReleaseTranslation(PointF initialTranslation, BubbleBarLocation location)535     public PointF getBubbleBarDragReleaseTranslation(PointF initialTranslation,
536             BubbleBarLocation location) {
537         return mBarView.getBubbleBarDragReleaseTranslation(initialTranslation, location);
538     }
539 
540     /**
541      * Get translation for bubble view when drag is released.
542      *
543      * @see BubbleBarView#getDraggedBubbleReleaseTranslation(PointF, BubbleBarLocation)
544      */
getDraggedBubbleReleaseTranslation(PointF initialTranslation, BubbleBarLocation location)545     public PointF getDraggedBubbleReleaseTranslation(PointF initialTranslation,
546             BubbleBarLocation location) {
547         if (location == mBarView.getBubbleBarLocation()) {
548             return initialTranslation;
549         }
550         return mBarView.getDraggedBubbleReleaseTranslation(initialTranslation, location);
551     }
552 
553     /**
554      * Called when bubble was dragged into the dismiss target. Notifies System
555      * @param bubble dismissed bubble item
556      */
onDismissBubbleWhileDragging(@onNull BubbleBarItem bubble)557     public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
558         mSystemUiProxy.dragBubbleToDismiss(bubble.getKey());
559     }
560 
561     /**
562      * Called when bubble stack was dragged into the dismiss target
563      */
onDismissAllBubblesWhileDragging()564     public void onDismissAllBubblesWhileDragging() {
565         mSystemUiProxy.removeAllBubbles();
566     }
567 
568     /**
569      * Set listener to be notified when bubble bar bounds have changed
570      */
setBoundsChangeListener(@ullable BubbleBarBoundsChangeListener listener)571     public void setBoundsChangeListener(@Nullable BubbleBarBoundsChangeListener listener) {
572         mBoundsChangeListener = listener;
573     }
574 
575     /**
576      * Listener to receive updates about bubble bar bounds changing
577      */
578     public interface BubbleBarBoundsChangeListener {
579         /** Called when bounds have changed */
onBoundsChanged()580         void onBoundsChanged();
581     }
582 }
583