• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static com.android.launcher3.Utilities.mapRange;
22 import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT;
23 import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT;
24 
25 import android.animation.Animator;
26 import android.animation.AnimatorSet;
27 import android.content.Intent;
28 import android.content.pm.ShortcutInfo;
29 import android.content.res.Resources;
30 import android.graphics.Point;
31 import android.graphics.PointF;
32 import android.graphics.Rect;
33 import android.util.DisplayMetrics;
34 import android.util.Log;
35 import android.util.TypedValue;
36 import android.view.MotionEvent;
37 import android.view.View;
38 import android.widget.FrameLayout;
39 
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 
43 import com.android.app.animation.Interpolators;
44 import com.android.launcher3.AbstractFloatingView;
45 import com.android.launcher3.DeviceProfile;
46 import com.android.launcher3.R;
47 import com.android.launcher3.anim.AnimatedFloat;
48 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
49 import com.android.launcher3.dragndrop.DragController;
50 import com.android.launcher3.model.data.ItemInfo;
51 import com.android.launcher3.model.data.WorkspaceItemInfo;
52 import com.android.launcher3.taskbar.TaskbarActivityContext;
53 import com.android.launcher3.taskbar.TaskbarControllers;
54 import com.android.launcher3.taskbar.TaskbarInsetsController;
55 import com.android.launcher3.taskbar.TaskbarSharedState;
56 import com.android.launcher3.taskbar.TaskbarStashController;
57 import com.android.launcher3.taskbar.bubbles.BubbleBarLocationDropTarget.BubbleBarDragListener;
58 import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator;
59 import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController;
60 import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner;
61 import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks;
62 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
63 import com.android.launcher3.util.DisplayController;
64 import com.android.launcher3.util.MultiPropertyFactory;
65 import com.android.launcher3.util.MultiValueAlpha;
66 import com.android.quickstep.SystemUiProxy;
67 import com.android.wm.shell.Flags;
68 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
69 import com.android.wm.shell.shared.bubbles.DeviceConfig;
70 
71 import java.io.PrintWriter;
72 import java.util.List;
73 import java.util.Objects;
74 import java.util.function.Consumer;
75 
76 /**
77  * Controller for {@link BubbleBarView}. Manages the visibility of the bubble bar as well as
78  * responding to changes in bubble state provided by BubbleBarController.
79  */
80 public class BubbleBarViewController {
81 
82     private static final String TAG = "BubbleBarViewController";
83     private static final float APP_ICON_SMALL_DP = 44f;
84     private static final float APP_ICON_MEDIUM_DP = 48f;
85     private static final float APP_ICON_LARGE_DP = 52f;
86     /** The dot size is defined as a percentage of the icon size. */
87     private static final float DOT_TO_BUBBLE_SIZE_RATIO = 0.228f;
88     public static final int TASKBAR_FADE_IN_DURATION_MS = 150;
89     public static final int TASKBAR_FADE_IN_DELAY_MS = 50;
90     public static final int TASKBAR_FADE_OUT_DURATION_MS = 100;
91     private final SystemUiProxy mSystemUiProxy;
92     private final TaskbarActivityContext mActivity;
93     private final BubbleBarView mBarView;
94     private int mIconSize;
95     private int mBubbleBarPadding;
96     private final int mDragElevation;
97 
98     // Initialized in init.
99     private BubbleStashController mBubbleStashController;
100     private BubbleBarController mBubbleBarController;
101     private BubbleDragController mBubbleDragController;
102     private TaskbarStashController mTaskbarStashController;
103     private TaskbarInsetsController mTaskbarInsetsController;
104     private TaskbarViewPropertiesProvider mTaskbarViewPropertiesProvider;
105     private View.OnClickListener mBubbleClickListener;
106     private BubbleView.Controller mBubbleViewController;
107     private BubbleBarOverflow mOverflowBubble;
108 
109     // These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing
110     private final MultiValueAlpha mBubbleBarAlpha;
111     private final AnimatedFloat mBubbleBarBubbleAlpha = new AnimatedFloat(this::updateBubbleAlpha);
112     private final AnimatedFloat mBubbleBarBackgroundAlpha = new AnimatedFloat(
113             this::updateBackgroundAlpha);
114     private final AnimatedFloat mBubbleBarScaleX = new AnimatedFloat(this::updateScaleX);
115     private final AnimatedFloat mBubbleBarScaleY = new AnimatedFloat(this::updateScaleY);
116     private final AnimatedFloat mBubbleBarBackgroundScaleX = new AnimatedFloat(
117             this::updateBackgroundScaleX);
118     private final AnimatedFloat mBubbleBarBackgroundScaleY = new AnimatedFloat(
119             this::updateBackgroundScaleY);
120     private final AnimatedFloat mBubbleBarTranslationY = new AnimatedFloat(
121             this::updateTranslationY);
122     private final AnimatedFloat mBubbleOffsetY = new AnimatedFloat(
123             this::updateBubbleOffsetY);
124     private final AnimatedFloat mBubbleBarPinning = new AnimatedFloat(pinningProgress -> {
125         updateTranslationY();
126         setBubbleBarScaleAndPadding(pinningProgress);
127     });
128     private final BubbleBarDragListener mDragListener = new BubbleBarDragListener() {
129 
130         @Override
131         public void getBubbleBarLocationHitRect(@NonNull BubbleBarLocation bubbleBarLocation,
132                 Rect outRect) {
133             Point screenSize = DisplayController.INSTANCE.get(mActivity).getInfo().currentSize;
134             outRect.top = screenSize.y - mBubbleBarDropTargetSize;
135             outRect.bottom = screenSize.y;
136             if (bubbleBarLocation.isOnLeft(mBarView.isLayoutRtl())) {
137                 outRect.left = 0;
138                 outRect.right = mBubbleBarDropTargetSize;
139             } else {
140                 outRect.left = screenSize.x - mBubbleBarDropTargetSize;
141                 outRect.right = screenSize.x;
142             }
143         }
144 
145         @Override
146         public void onLauncherItemDroppedOverBubbleBarDragZone(@NonNull BubbleBarLocation location,
147                 @NonNull ItemInfo itemInfo) {
148             AbstractFloatingView.closeAllOpenViews(mActivity);
149             if (itemInfo instanceof WorkspaceItemInfo) {
150                 ShortcutInfo shortcutInfo = ((WorkspaceItemInfo) itemInfo).getDeepShortcutInfo();
151                 if (shortcutInfo != null) {
152                     mSystemUiProxy.showShortcutBubble(shortcutInfo, location);
153                     return;
154                 }
155             }
156             Intent itemIntent = itemInfo.getIntent();
157             if (itemIntent != null && itemIntent.getComponent() != null) {
158                 itemIntent.setPackage(itemIntent.getComponent().getPackageName());
159                 mSystemUiProxy.showAppBubble(itemIntent, itemInfo.user, location);
160             }
161         }
162 
163         @Override
164         public void onLauncherItemDraggedOutsideBubbleBarDropZone() {
165             onItemDraggedOutsideBubbleBarDropZone();
166             mSystemUiProxy.showBubbleDropTarget(/* show = */ false);
167         }
168 
169         @Override
170         public void onLauncherItemDraggedOverBubbleBarDragZone(
171                 @NonNull BubbleBarLocation location) {
172             onDragItemOverBubbleBarDragZone(location);
173             mSystemUiProxy.showBubbleDropTarget(/* show = */ true, location);
174         }
175 
176         @NonNull
177         @Override
178         public View getDropView() {
179             return mBarView;
180         }
181     };
182 
183     // Modified when swipe up is happening on the bubble bar or task bar.
184     private float mBubbleBarSwipeUpTranslationY;
185     // Modified when bubble bar is springing back into the stash handle.
186     private float mBubbleBarStashTranslationY;
187     // Minimum distance between the BubbleBar and the taskbar
188     private final int mBubbleBarTaskbarMinDistance;
189     // Whether the bar is hidden for a sysui state.
190     private boolean mHiddenForSysui;
191     // Whether the bar is hidden because there are no bubbles.
192     private boolean mHiddenForNoBubbles = true;
193     // Whether the bar is hidden when stashed
194     private boolean mHiddenForStashed;
195     private boolean mShouldShowEducation;
196     public boolean mOverflowAdded;
197     private boolean mWasStashedBeforeEnteringBubbleDragZone = false;
198 
199     /** This field is used solely to track the bubble bar location prior to the start of the drag */
200     private @Nullable BubbleBarLocation mBubbleBarDragLocation;
201 
202     private BubbleBarViewAnimator mBubbleBarViewAnimator;
203     private final FrameLayout mBubbleBarContainer;
204     private BubbleBarFlyoutController mBubbleBarFlyoutController;
205     private BubbleBarPinController mBubbleBarPinController;
206     private TaskbarSharedState mTaskbarSharedState;
207     private final BubbleBarLocationDropTarget mBubbleBarLeftDropTarget;
208     private final BubbleBarLocationDropTarget mBubbleBarRightDropTarget;
209     private final TimeSource mTimeSource = System::currentTimeMillis;
210     private final int mTaskbarTranslationDelta;
211     private final int mBubbleBarDropTargetSize;
212 
213     @Nullable
214     private BubbleBarBoundsChangeListener mBoundsChangeListener;
215 
BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView, FrameLayout bubbleBarContainer)216     public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView,
217             FrameLayout bubbleBarContainer) {
218         mActivity = activity;
219         mBarView = barView;
220         mBubbleBarContainer = bubbleBarContainer;
221         mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity);
222         mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
223         Resources res = activity.getResources();
224         mIconSize = res.getDimensionPixelSize(R.dimen.bubblebar_icon_size);
225         mBubbleBarTaskbarMinDistance = res.getDimensionPixelSize(
226                 R.dimen.bubblebar_transient_taskbar_min_distance);
227         mDragElevation = res.getDimensionPixelSize(R.dimen.dragged_bubble_elevation);
228         mTaskbarTranslationDelta = getBubbleBarTranslationDeltaForTaskbar(activity);
229         if (DeviceConfig.isSmallTablet(mActivity)) {
230             mBubbleBarDropTargetSize = res.getDimensionPixelSize(R.dimen.drag_zone_bubble_fold);
231         } else {
232             mBubbleBarDropTargetSize = res.getDimensionPixelSize(R.dimen.drag_zone_bubble_tablet);
233         }
234         mBubbleBarLeftDropTarget = new BubbleBarLocationDropTarget(BubbleBarLocation.LEFT,
235                 mDragListener);
236         mBubbleBarRightDropTarget = new BubbleBarLocationDropTarget(BubbleBarLocation.RIGHT,
237                 mDragListener);
238     }
239 
240     /** Initializes controller. */
init(TaskbarControllers controllers, BubbleControllers bubbleControllers, TaskbarViewPropertiesProvider taskbarViewPropertiesProvider)241     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers,
242             TaskbarViewPropertiesProvider taskbarViewPropertiesProvider) {
243         mTaskbarSharedState = controllers.getSharedState();
244         mBubbleStashController = bubbleControllers.bubbleStashController;
245         mBubbleBarController = bubbleControllers.bubbleBarController;
246         mBubbleDragController = bubbleControllers.bubbleDragController;
247         mBubbleBarPinController = bubbleControllers.bubbleBarPinController;
248         mTaskbarStashController = controllers.taskbarStashController;
249         mTaskbarInsetsController = controllers.taskbarInsetsController;
250         mBubbleBarFlyoutController = new BubbleBarFlyoutController(
251                 mBubbleBarContainer, createFlyoutPositioner(), createFlyoutCallbacks());
252         mBubbleBarViewAnimator = new BubbleBarViewAnimator(
253                 mBarView, mBubbleStashController, mBubbleBarFlyoutController,
254                 createBubbleBarParentViewController(), mBubbleBarController::showExpandedView,
255                 () -> setHiddenForBubbles(false));
256         mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider;
257         onBubbleBarConfigurationChanged(/* animate= */ false);
258         mActivity.addOnDeviceProfileChangeListener(
259                 dp -> onBubbleBarConfigurationChanged(/* animate= */ true));
260         mBubbleBarScaleY.updateValue(1f);
261         mBubbleClickListener = v -> onBubbleClicked((BubbleView) v);
262         mBubbleDragController.setupBubbleBarView(mBarView);
263         mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView);
264         if (!Flags.enableOptionalBubbleOverflow()) {
265             showOverflow(true);
266         }
267         if (!mBubbleStashController.isTransientTaskBar()) {
268             // TODO(b/380274085) for transient taskbar mode, the click is also handled by the input
269             //  consumer. This check can be removed once b/380274085 is fixed.
270             mBarView.setOnClickListener(v -> setExpanded(!mBarView.isExpanded()));
271         }
272         mBarView.addOnLayoutChangeListener(
273                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
274                     mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
275                     if (mBoundsChangeListener != null) {
276                         mBoundsChangeListener.onBoundsChanged();
277                     }
278                 });
279         float pinningValue = mActivity.isTransientTaskbar()
280                 ? PINNING_TRANSIENT
281                 : PINNING_PERSISTENT;
282         mBubbleBarPinning.updateValue(pinningValue);
283         mBarView.setController(new BubbleBarView.Controller() {
284             @Override
285             public float getBubbleBarTranslationY() {
286                 return mBubbleStashController.getBubbleBarTranslationY();
287             }
288 
289             @Override
290             public void onBubbleBarTouched() {
291                 if (isAnimatingNewBubble()) {
292                     interruptAnimationForTouch();
293                 }
294             }
295 
296             @Override
297             public void expandBubbleBar() {
298                 BubbleBarViewController.this.setExpanded(
299                         /* isExpanded= */ true, /* maybeShowEdu*/ true);
300             }
301 
302             @Override
303             public void dismissBubbleBar() {
304                 onDismissAllBubbles();
305             }
306 
307             @Override
308             public void updateBubbleBarLocation(BubbleBarLocation location,
309                     @BubbleBarLocation.UpdateSource int source) {
310                 mBubbleBarController.updateBubbleBarLocation(location, source);
311             }
312 
313             @Override
314             public void setIsDragging(boolean dragging) {
315                 mBubbleBarContainer.setElevation(dragging ? mDragElevation : 0);
316             }
317         });
318 
319         mBubbleViewController = new BubbleView.Controller() {
320             @Override
321             public BubbleBarLocation getBubbleBarLocation() {
322                 return BubbleBarViewController.this.getBubbleBarLocation();
323             }
324 
325             @Override
326             public void dismiss(BubbleView bubble) {
327                 if (bubble.getBubble() != null) {
328                     notifySysUiBubbleDismissed(bubble.getBubble());
329                 }
330                 onBubbleDismissed(bubble);
331             }
332 
333             @Override
334             public void collapse() {
335                 collapseBubbleBar();
336             }
337 
338             @Override
339             public void updateBubbleBarLocation(BubbleBarLocation location,
340                     @BubbleBarLocation.UpdateSource int source) {
341                 mBubbleBarController.updateBubbleBarLocation(location, source);
342             }
343         };
344     }
345 
346     /** Adds bubble bar locations drop zones to the drag controller. */
addBubbleBarDropTargets(DragController<?> dragController)347     public void addBubbleBarDropTargets(DragController<?> dragController) {
348         dragController.addDropTarget(mBubbleBarLeftDropTarget);
349         dragController.addDropTarget(mBubbleBarRightDropTarget);
350     }
351 
352     /** Removes bubble bar locations drop zones to the drag controller. */
removeBubbleBarDropTargets(DragController<?> dragController)353     public void removeBubbleBarDropTargets(DragController<?> dragController) {
354         dragController.removeDropTarget(mBubbleBarLeftDropTarget);
355         dragController.removeDropTarget(mBubbleBarRightDropTarget);
356     }
357 
358     /** Returns animated float property responsible for pinning transition animation. */
getBubbleBarPinning()359     public AnimatedFloat getBubbleBarPinning() {
360         return mBubbleBarPinning;
361     }
362 
createFlyoutPositioner()363     private BubbleBarFlyoutPositioner createFlyoutPositioner() {
364         return new BubbleBarFlyoutPositioner() {
365 
366             @Override
367             public boolean isOnLeft() {
368                 boolean shouldRevertLocation =
369                         mBarView.isShowingDropTarget() && isLocationUpdatedForDropTarget();
370                 boolean isOnLeft = mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl());
371                 return shouldRevertLocation != isOnLeft;
372             }
373 
374             @Override
375             public float getTargetTy() {
376                 return mBarView.getTranslationY() - mBarView.getHeight();
377             }
378 
379             @Override
380             @NonNull
381             public PointF getDistanceToCollapsedPosition() {
382                 // the flyout animates from the selected bubble dot. calculate the distance it needs
383                 // to translate itself to its starting position.
384                 PointF distanceToDotCenter = mBarView.getSelectedBubbleDotDistanceFromTopLeft();
385 
386                 // if we're gravitating left, return the distance between the top left corner of the
387                 // bubble bar and the bottom left corner of the dot.
388                 // if we're gravitating right, return the distance between the top right corner of
389                 // the bubble bar and the bottom right corner of the dot.
390                 float distanceX = isOnLeft()
391                         ? distanceToDotCenter.x - getCollapsedSize() / 2
392                         : mBarView.getWidth() - distanceToDotCenter.x - getCollapsedSize() / 2;
393                 float distanceY = distanceToDotCenter.y + getCollapsedSize() / 2;
394                 return new PointF(distanceX, distanceY);
395             }
396 
397             @Override
398             public float getCollapsedSize() {
399                 return mIconSize * DOT_TO_BUBBLE_SIZE_RATIO;
400             }
401 
402             @Override
403             public int getCollapsedColor() {
404                 return mBarView.getSelectedBubbleDotColor();
405             }
406 
407             @Override
408             public float getCollapsedElevation() {
409                 return mBarView.getBubbleElevation();
410             }
411 
412             @Override
413             public float getDistanceToRevealTriangle() {
414                 return getDistanceToCollapsedPosition().y - mBarView.getPointerSize();
415             }
416         };
417     }
418 
419     private FlyoutCallbacks createFlyoutCallbacks() {
420         return new FlyoutCallbacks() {
421             @Override
422             public void flyoutClicked() {
423                 interruptAnimationForTouch();
424                 setExpanded(/* isExpanded= */ true, /* maybeShowEdu*/ true);
425             }
426         };
427     }
428 
429     private BubbleBarParentViewHeightUpdateNotifier createBubbleBarParentViewController() {
430         return new BubbleBarParentViewHeightUpdateNotifier() {
431             @Override
432             public void updateTopBoundary() {
433                 mActivity.setTaskbarWindowForAnimatingBubble();
434             }
435         };
436     }
437 
438     private void onBubbleClicked(BubbleView bubbleView) {
439         if (mBubbleBarPinning.isAnimating()) return;
440         bubbleView.markSeen();
441         BubbleBarItem bubble = bubbleView.getBubble();
442         if (bubble == null) {
443             Log.e(TAG, "bubble click listener, bubble was null");
444         }
445 
446         final String currentlySelected = mBubbleBarController.getSelectedBubbleKey();
447         if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) {
448             // Tapping the currently selected bubble while expanded collapses the view.
449             collapseBubbleBar();
450         } else {
451             mBubbleBarController.showAndSelectBubble(bubble);
452         }
453     }
454 
455     /** Interrupts the running animation for a touch event on the bubble bar or flyout. */
456     private void interruptAnimationForTouch() {
457         mBubbleBarViewAnimator.interruptForTouch();
458         mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY());
459     }
460 
461     private void collapseBubbleBar() {
462         setExpanded(false);
463         mBubbleStashController.stashBubbleBar();
464     }
465 
466     /** Notifies that the stash state is changing. */
467     public void onStashStateChanging() {
468         if (isAnimatingNewBubble()) {
469             mBubbleBarViewAnimator.onStashStateChangingWhileAnimating();
470         }
471     }
472 
473     /** Shows the education view if it was previously requested. */
474     private boolean maybeShowEduView() {
475         if (mShouldShowEducation) {
476             mShouldShowEducation = false;
477             // Get the bubble bar bounds on screen
478             Rect bounds = new Rect();
479             mBarView.getBoundsOnScreen(bounds);
480             // Calculate user education reference position in Screen coordinates
481             Point position = new Point(bounds.centerX(), bounds.top);
482             // Show user education relative to the reference point
483             mSystemUiProxy.showUserEducation(position);
484             return true;
485         }
486         return false;
487     }
488 
489     /** Notifies that the IME became visible. */
490     public void onImeVisible() {
491         if (isAnimatingNewBubble()) {
492             mBubbleBarViewAnimator.interruptForIme();
493         }
494     }
495 
496     //
497     // The below animators are exposed to BubbleStashController so it can manage the stashing
498     // animation.
499     //
500 
501     public MultiPropertyFactory<View> getBubbleBarAlpha() {
502         return mBubbleBarAlpha;
503     }
504 
505     public AnimatedFloat getBubbleBarBubbleAlpha() {
506         return mBubbleBarBubbleAlpha;
507     }
508 
509     public AnimatedFloat getBubbleBarBackgroundAlpha() {
510         return mBubbleBarBackgroundAlpha;
511     }
512 
513     public AnimatedFloat getBubbleBarScaleX() {
514         return mBubbleBarScaleX;
515     }
516 
517     public AnimatedFloat getBubbleBarScaleY() {
518         return mBubbleBarScaleY;
519     }
520 
521     public AnimatedFloat getBubbleBarBackgroundScaleX() {
522         return mBubbleBarBackgroundScaleX;
523     }
524 
525     public AnimatedFloat getBubbleBarBackgroundScaleY() {
526         return mBubbleBarBackgroundScaleY;
527     }
528 
529     public AnimatedFloat getBubbleBarTranslationY() {
530         return mBubbleBarTranslationY;
531     }
532 
533     public AnimatedFloat getBubbleOffsetY() {
534         return mBubbleOffsetY;
535     }
536 
537     public float getBubbleBarCollapsedWidth() {
538         return mBarView.collapsedWidth();
539     }
540 
541     public float getBubbleBarCollapsedHeight() {
542         return mBarView.getBubbleBarCollapsedHeight();
543     }
544 
545     /** Returns the bubble bar arrow height.*/
546     public float getBubbleBarArrowHeight() {
547         return mBarView.getArrowHeight();
548     }
549 
550     /**
551      * @see BubbleBarView#getRelativePivotX()
552      */
553     public float getBubbleBarRelativePivotX() {
554         return mBarView.getRelativePivotX();
555     }
556 
557     /**
558      * @see BubbleBarView#getRelativePivotY()
559      */
560     public float getBubbleBarRelativePivotY() {
561         return mBarView.getRelativePivotY();
562     }
563 
564     /**
565      * @see BubbleBarView#setRelativePivot(float, float)
566      */
567     public void setBubbleBarRelativePivot(float x, float y) {
568         mBarView.setRelativePivot(x, y);
569     }
570 
571     /**
572      * Whether the bubble bar is visible or not.
573      */
574     public boolean isBubbleBarVisible() {
575         return mBarView.getVisibility() == VISIBLE;
576     }
577 
578     /** Whether the bubble bar has bubbles. */
579     public boolean hasBubbles() {
580         return mBarView.getBubbleChildCount() > 0;
581     }
582 
583     /**
584      * @return current {@link BubbleBarLocation}
585      */
586     public BubbleBarLocation getBubbleBarLocation() {
587         return mBarView.getBubbleBarLocation();
588     }
589 
590     /**
591      * @return the max collapsed width for the bubble bar.
592      */
593     public float getCollapsedWidthWithMaxVisibleBubbles() {
594         return mBarView.getCollapsedWidthWithMaxVisibleBubbles();
595     }
596 
597     /**
598      * @return {@code true} if bubble bar is on the left edge of the screen, {@code false} if on
599      * the right
600      */
601     public boolean isBubbleBarOnLeft() {
602         return mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl());
603     }
604 
605     /**
606      * Update bar {@link BubbleBarLocation}
607      */
608     public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
609         mBarView.setBubbleBarLocation(bubbleBarLocation);
610     }
611 
612     /**
613      * Animate bubble bar to the given location. The location change is transient. It does not
614      * update the state of the bubble bar.
615      * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}.
616      */
617     public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
618         mBarView.animateToBubbleBarLocation(bubbleBarLocation);
619     }
620 
621     /** Return animator for animating bubble bar in. */
622     public Animator animateBubbleBarLocationIn(BubbleBarLocation fromLocation,
623             BubbleBarLocation toLocation) {
624         return mBarView.animateToBubbleBarLocationIn(fromLocation, toLocation);
625     }
626 
627     /** Return animator for animating bubble bar out. */
628     public Animator animateBubbleBarLocationOut(BubbleBarLocation toLocation) {
629         return mBarView.animateToBubbleBarLocationOut(toLocation);
630     }
631 
632     /** Returns whether the Bubble Bar is currently displaying a drop target. */
633     public boolean isShowingDropTarget() {
634         return mBarView.isShowingDropTarget();
635     }
636 
637     /**
638      * Notifies the controller that a drag event is over the Bubble Bar drop zone. The controller
639      * will display the appropriate drop target and enter drop target mode. The controller will also
640      * update the return value of {@link #isLocationUpdatedForDropTarget()} to true if location was
641      * updated.
642      */
643     public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) {
644         mBubbleBarDragLocation = bubbleBarLocation;
645         mBarView.showDropTarget(/* isDropTarget = */ true);
646         mWasStashedBeforeEnteringBubbleDragZone = hasBubbles()
647             && mBubbleStashController.isStashed();
648         if (mWasStashedBeforeEnteringBubbleDragZone) {
649             // bubble bar is stashed - un-stash at drag location
650             mBubbleStashController.showBubbleBarAtLocation(
651                     /* fromLocation = */ getBubbleBarLocation(),
652                     /* toLocation = */  mBubbleBarDragLocation
653             );
654         } else if (hasBubbles()) {
655             if (isLocationUpdatedForDropTarget()) {
656                 // bubble bar has bubbles and location is changed - animate bar to the opposite side
657                 animateBubbleBarLocation(bubbleBarLocation);
658             }
659         } else {
660             // bubble bar has no bubbles flow just show the empty drop target
661             mBubbleBarPinController.showDropTarget(bubbleBarLocation);
662         }
663     }
664 
665     /**
666      * Returns {@code true} if location was updated after most recent
667      * {@link #onDragItemOverBubbleBarDragZone}}.
668      */
669     public boolean isLocationUpdatedForDropTarget() {
670         if (mBubbleBarDragLocation == null) {
671             return false;
672         }
673         boolean isRtl = mBarView.isLayoutRtl();
674         return getBubbleBarLocation().isOnLeft(isRtl)
675                 != mBubbleBarDragLocation.isOnLeft(isRtl);
676     }
677 
678     /**
679      * Notifies the controller that the drag event is outside the Bubble Bar drop zone.
680      * This will hide the drop target zone if there are no bubbles or return the
681      * Bubble Bar to its original location. The controller will also exit drop target
682      * mode and reset the value returned from {@link #isLocationUpdatedForDropTarget()} to false.
683      */
684     public void onItemDraggedOutsideBubbleBarDropZone() {
685         if (!isShowingDropTarget()) {
686             return;
687         }
688         if (mWasStashedBeforeEnteringBubbleDragZone && mBubbleBarDragLocation != null) {
689             // bubble bar was stashed - stash at original location
690             mBubbleStashController.stashBubbleBarToLocation(
691                     /* fromLocation = */ mBubbleBarDragLocation,
692                     /* toLocation = */ getBubbleBarLocation()
693             );
694         } else if (hasBubbles()) {
695             if (isLocationUpdatedForDropTarget()) {
696                 // bubble bar has bubbles and location was changed - return to the original
697                 // location
698                 animateBubbleBarLocation(getBubbleBarLocation());
699             }
700         }
701         onItemDragCompleted();
702     }
703 
704     /**
705      * Notifies the controller that the drag has completed over the Bubble Bar drop zone.
706      * The controller will hide the drop target if there are no bubbles and exit drop target mode.
707      */
708     public void onItemDragCompleted() {
709         mBarView.showDropTarget(/* isDropTarget = */ false);
710         mBubbleBarPinController.hideDropTarget();
711         mWasStashedBeforeEnteringBubbleDragZone = false;
712         mBubbleBarDragLocation = null;
713     }
714 
715     /**
716      * The bounds of the bubble bar.
717      */
718     public Rect getBubbleBarBounds() {
719         return mBarView.getBubbleBarBounds();
720     }
721 
722     /** Returns the bounds of the flyout view if it exists, or {@code null} otherwise. */
723     @Nullable
724     public Rect getFlyoutBounds() {
725         return mBubbleBarFlyoutController.getFlyoutBounds();
726     }
727 
728     /** Checks that bubble bar is visible and that the motion event is within bounds. */
729     public boolean isEventOverBubbleBar(MotionEvent event) {
730         if (!isBubbleBarVisible()) return false;
731         final Rect bounds = getBubbleBarBounds();
732         final int bubbleBarTopOnScreen = mBarView.getRestingTopPositionOnScreen();
733         final float x = event.getX();
734         return event.getRawY() >= bubbleBarTopOnScreen && x >= bounds.left && x <= bounds.right;
735     }
736 
737     /** Whether a new bubble is animating. */
738     public boolean isAnimatingNewBubble() {
739         return mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.isAnimating();
740     }
741 
742     public boolean isNewBubbleAnimationRunningOrPending() {
743         return mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.hasAnimation();
744     }
745 
746     /** The horizontal margin of the bubble bar from the edge of the screen. */
747     public int getHorizontalMargin() {
748         return mBarView.getHorizontalMargin();
749     }
750 
751     /**
752      * When the bubble bar is not stashed, it can be collapsed (the icons are in a stack) or
753      * expanded (the icons are in a row). This indicates whether the bubble bar is expanded.
754      */
755     public boolean isExpanded() {
756         return mBarView.isExpanded();
757     }
758 
759     /**
760      * Whether the motion event is within the bounds of the bubble bar.
761      */
762     public boolean isEventOverAnyItem(MotionEvent ev) {
763         return mBarView.isEventOverAnyItem(ev);
764     }
765 
766     //
767     // Visibility of the bubble bar
768     //
769 
770     /**
771      * Returns whether the bubble bar is hidden because there are no bubbles.
772      */
773     public boolean isHiddenForNoBubbles() {
774         return mHiddenForNoBubbles;
775     }
776 
777     /** Returns maximum height of the bubble bar with the flyout view. */
778     public int getBubbleBarWithFlyoutMaximumHeight() {
779         if (!hasBubbles() && !isAnimatingNewBubble()) return 0;
780         int bubbleBarTopOnHome = (int) (mBubbleStashController.getBubbleBarVerticalCenterForHome()
781                 + mBarView.getBubbleBarCollapsedHeight() / 2 + mBarView.getArrowHeight());
782         if (isAnimatingNewBubble()) {
783             if (mTaskbarStashController.isInApp() && mBubbleStashController.getHasHandleView()) {
784                 // when animating a bubble in an app, the bubble bar will be higher than its
785                 // position on home
786                 float bubbleBarTopDistanceFromBottom =
787                         -mBubbleStashController.getBubbleBarTranslationYForTaskbar()
788                                 + mBarView.getHeight();
789                 return (int) bubbleBarTopDistanceFromBottom
790                         + mBubbleBarFlyoutController.getMaximumFlyoutHeight();
791             }
792             return bubbleBarTopOnHome + mBubbleBarFlyoutController.getMaximumFlyoutHeight();
793         } else {
794             return bubbleBarTopOnHome;
795         }
796     }
797 
798     /**
799      * Sets whether the bubble bar should be hidden because there are no bubbles.
800      */
801     public void setHiddenForBubbles(boolean hidden) {
802         if (mHiddenForNoBubbles != hidden) {
803             mHiddenForNoBubbles = hidden;
804             if (hidden) {
805                 mBarView.dismiss(() -> {
806                     updateVisibilityForStateChange();
807                     mBarView.setExpanded(false);
808                     adjustTaskbarAndHotseatToBubbleBarState(/* isBubbleBarExpanded= */ false);
809                     mActivity.bubbleBarVisibilityChanged(/* isVisible= */ false);
810                 });
811             } else {
812                 updateVisibilityForStateChange();
813                 mActivity.bubbleBarVisibilityChanged(/* isVisible= */ true);
814             }
815         }
816     }
817 
818     /** Sets a callback that updates the selected bubble after the bubble bar collapses. */
819     public void setUpdateSelectedBubbleAfterCollapse(
820             Consumer<String> updateSelectedBubbleAfterCollapse) {
821         mBarView.setUpdateSelectedBubbleAfterCollapse(updateSelectedBubbleAfterCollapse);
822     }
823 
824     /** Returns whether the bubble bar should be hidden because of the current sysui state. */
825     boolean isHiddenForSysui() {
826         return mHiddenForSysui;
827     }
828 
829     /**
830      * Sets whether the bubble bar should be hidden due to SysUI state (e.g. on lockscreen).
831      */
832     public void setHiddenForSysui(boolean hidden) {
833         if (mHiddenForSysui != hidden) {
834             mHiddenForSysui = hidden;
835             updateVisibilityForStateChange();
836         }
837     }
838 
839     /** Sets whether the bubble bar should be hidden due to stashed state */
840     public void setHiddenForStashed(boolean hidden) {
841         if (mHiddenForStashed != hidden) {
842             mHiddenForStashed = hidden;
843             updateVisibilityForStateChange();
844         }
845     }
846 
847     private void updateVisibilityForStateChange() {
848         boolean hiddenForStashedAndNotAnimating =
849                 mHiddenForStashed && !mBubbleBarViewAnimator.isAnimating();
850         if (mHiddenForSysui || mHiddenForNoBubbles || hiddenForStashedAndNotAnimating) {
851             //TODO(b/404870188) this visibility change cause search view drag misbehavior
852             mBarView.setVisibility(INVISIBLE);
853         } else {
854             mBarView.setVisibility(VISIBLE);
855         }
856     }
857 
858     /**
859      * Returns the translation X of the transient taskbar according to the bubble bar location
860      * regardless of the current taskbar mode.
861      */
862     public int getTransientTaskbarTranslationXForBubbleBar(BubbleBarLocation location) {
863         int taskbarShift = 0;
864         if (!isBubbleBarVisible() || mTaskbarViewPropertiesProvider == null) return taskbarShift;
865         Rect taskbarViewBounds = mTaskbarViewPropertiesProvider.getTaskbarViewBounds();
866         if (taskbarViewBounds.isEmpty()) return taskbarShift;
867         int actualDistance =
868                 getDistanceBetweenTransientTaskbarAndBubbleBar(location, taskbarViewBounds);
869         if (actualDistance < mBubbleBarTaskbarMinDistance) {
870             taskbarShift = mBubbleBarTaskbarMinDistance - actualDistance;
871             if (!location.isOnLeft(mBarView.isLayoutRtl())) {
872                 taskbarShift = -taskbarShift;
873             }
874         }
875         return taskbarShift;
876     }
877 
878     private int getDistanceBetweenTransientTaskbarAndBubbleBar(BubbleBarLocation location,
879             Rect taskbarViewBounds) {
880         Resources res = mActivity.getResources();
881         DeviceProfile transientDp = mActivity.getTransientTaskbarDeviceProfile();
882         int transientIconSize = getBubbleBarIconSizeFromDeviceProfile(res, transientDp);
883         int transientPadding = getBubbleBarPaddingFromDeviceProfile(res, transientDp);
884         int transientWidthWithMargin = (int) (mBarView.getCollapsedWidthForIconSizeAndPadding(
885                 transientIconSize, transientPadding) + mBarView.getHorizontalMargin());
886         int distance;
887         if (location.isOnLeft(mBarView.isLayoutRtl())) {
888             distance = taskbarViewBounds.left - transientWidthWithMargin;
889         } else {
890             int displayWidth = res.getDisplayMetrics().widthPixels;
891             int bubbleBarLeft = displayWidth - transientWidthWithMargin;
892             distance = bubbleBarLeft - taskbarViewBounds.right;
893         }
894         return distance;
895     }
896 
897     //
898     // Modifying view related properties.
899     //
900 
901     /** Notifies controller of configuration change, so bubble bar can be adjusted */
902     public void onBubbleBarConfigurationChanged(boolean animate) {
903         int newIconSize;
904         int newPadding;
905         Resources res = mActivity.getResources();
906         if (mBubbleStashController.isBubblesShowingOnHome()
907                 || mBubbleStashController.isTransientTaskBar()) {
908             newIconSize = getBubbleBarIconSizeFromDeviceProfile(res);
909             newPadding = getBubbleBarPaddingFromDeviceProfile(res);
910         } else {
911             // the bubble bar is shown inside the persistent task bar, use preset sizes
912             newIconSize = res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar);
913             newPadding = res.getDimensionPixelSize(
914                     R.dimen.bubblebar_icon_spacing_persistent_taskbar);
915         }
916         updateBubbleBarIconSizeAndPadding(newIconSize, newPadding, animate);
917     }
918 
919     private int getBubbleBarIconSizeFromDeviceProfile(Resources res) {
920         return getBubbleBarIconSizeFromDeviceProfile(res, mActivity.getDeviceProfile());
921     }
922 
923     private int getBubbleBarIconSizeFromDeviceProfile(Resources res, DeviceProfile deviceProfile) {
924         DisplayMetrics dm = res.getDisplayMetrics();
925         float smallIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
926                 APP_ICON_SMALL_DP, dm);
927         float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
928                 APP_ICON_MEDIUM_DP, dm);
929         float smallMediumThreshold = (smallIconSize + mediumIconSize) / 2f;
930         int taskbarIconSize = deviceProfile.taskbarIconSize;
931         return taskbarIconSize <= smallMediumThreshold
932                 ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_small) :
933                 res.getDimensionPixelSize(R.dimen.bubblebar_icon_size);
934 
935     }
936 
937     private int getBubbleBarPaddingFromDeviceProfile(Resources res) {
938         return getBubbleBarPaddingFromDeviceProfile(res, mActivity.getDeviceProfile());
939     }
940 
941     private int getBubbleBarPaddingFromDeviceProfile(Resources res, DeviceProfile deviceProfile) {
942         DisplayMetrics dm = res.getDisplayMetrics();
943         float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
944                 APP_ICON_MEDIUM_DP, dm);
945         float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
946                 APP_ICON_LARGE_DP, dm);
947         float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f;
948         return deviceProfile.taskbarIconSize >= mediumLargeThreshold
949                 ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_large) :
950                 res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
951     }
952 
953     private void updateBubbleBarIconSizeAndPadding(int iconSize, int padding, boolean animate) {
954         if (mIconSize == iconSize && mBubbleBarPadding == padding) return;
955         mIconSize = iconSize;
956         mBubbleBarPadding = padding;
957         if (animate) {
958             mBarView.animateBubbleBarIconSize(iconSize, padding);
959         } else {
960             mBarView.setIconSizeAndPadding(iconSize, padding);
961         }
962     }
963 
964     /**
965      * Sets the translation of the bubble bar during the swipe up gesture.
966      */
967     public void setTranslationYForSwipe(float transY) {
968         mBubbleBarSwipeUpTranslationY = transY;
969         updateTranslationY();
970     }
971 
972     /**
973      * Sets the translation of the bubble bar during the stash animation.
974      */
975     public void setTranslationYForStash(float transY) {
976         mBubbleBarStashTranslationY = transY;
977         updateTranslationY();
978     }
979 
980     private void updateTranslationY() {
981         mBarView.setTranslationY(mBubbleBarTranslationY.value + mBubbleBarSwipeUpTranslationY
982                 + mBubbleBarStashTranslationY + getBubbleBarTranslationYForTaskbarPinning());
983     }
984 
985     /** Computes translation y for taskbar pinning. */
986     private float getBubbleBarTranslationYForTaskbarPinning() {
987         if (mTaskbarSharedState == null) return 0f;
988         float pinningProgress = mBubbleBarPinning.value;
989         if (mTaskbarSharedState.startTaskbarVariantIsTransient) {
990             return mapRange(pinningProgress, /* min = */ 0f, mTaskbarTranslationDelta);
991         } else {
992             return mapRange(pinningProgress, -mTaskbarTranslationDelta, /* max = */ 0f);
993         }
994     }
995 
996     private void setBubbleBarScaleAndPadding(float pinningProgress) {
997         Resources res = mActivity.getResources();
998         // determine icon scale for pinning
999         int persistentIconSize = res.getDimensionPixelSize(
1000                 R.dimen.bubblebar_icon_size_persistent_taskbar);
1001         int transientIconSize = getBubbleBarIconSizeFromDeviceProfile(res,
1002                 mActivity.getTransientTaskbarDeviceProfile());
1003         float pinningIconSize = mapRange(pinningProgress, transientIconSize, persistentIconSize);
1004 
1005         // determine bubble bar padding for pinning
1006         int persistentPadding = res.getDimensionPixelSize(
1007                 R.dimen.bubblebar_icon_spacing_persistent_taskbar);
1008         int transientPadding = getBubbleBarPaddingFromDeviceProfile(res,
1009                 mActivity.getTransientTaskbarDeviceProfile());
1010         float pinningPadding = mapRange(pinningProgress, transientPadding, persistentPadding);
1011         mBarView.setIconSizeAndPaddingForPinning(pinningIconSize, pinningPadding);
1012     }
1013 
1014     /**
1015      * Calculates the vertical difference in the bubble bar positions for pinned and transient
1016      * taskbar modes.
1017      */
1018     private int getBubbleBarTranslationDeltaForTaskbar(TaskbarActivityContext activity) {
1019         Resources res = activity.getResources();
1020         int persistentBubbleSize = res
1021                 .getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar);
1022         int persistentSpacingSize = res
1023                 .getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_persistent_taskbar);
1024         int persistentBubbleBarSize = persistentBubbleSize + persistentSpacingSize * 2;
1025         int persistentTaskbarHeight = activity.getPersistentTaskbarDeviceProfile().taskbarHeight;
1026         int persistentBubbleBarY = (persistentTaskbarHeight - persistentBubbleBarSize) / 2;
1027         int transientBubbleBarY = activity.getTransientTaskbarDeviceProfile().taskbarBottomMargin;
1028         return transientBubbleBarY - persistentBubbleBarY;
1029     }
1030 
1031     private void updateScaleX(float scale) {
1032         mBarView.setScaleX(scale);
1033     }
1034 
1035     private void updateScaleY(float scale) {
1036         mBarView.setScaleY(scale);
1037     }
1038 
1039     private void updateBackgroundScaleX(float scale) {
1040         mBarView.setBackgroundScaleX(scale);
1041     }
1042 
1043     private void updateBackgroundScaleY(float scale) {
1044         mBarView.setBackgroundScaleY(scale);
1045     }
1046 
1047     private void updateBubbleAlpha(float alpha) {
1048         mBarView.setBubbleAlpha(alpha);
1049     }
1050 
1051     private void updateBubbleOffsetY(float transY) {
1052         mBarView.setBubbleOffsetY(transY);
1053     }
1054 
1055     private void updateBackgroundAlpha(float alpha) {
1056         mBarView.setBackgroundAlpha(alpha);
1057     }
1058 
1059     //
1060     // Manipulating the specific bubble views in the bar
1061     //
1062 
1063     /**
1064      * Removes the provided bubble from the bubble bar.
1065      */
1066     public void removeBubble(BubbleBarBubble b) {
1067         if (b != null) {
1068             mBarView.removeBubble(b.getView());
1069             b.getView().setController(null);
1070         } else {
1071             Log.w(TAG, "removeBubble, bubble was null!");
1072         }
1073     }
1074 
1075     /** Adds a new bubble and removes an old bubble at the same time. */
1076     public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble, BubbleBarBubble removedBubble,
1077             @Nullable BubbleBarBubble bubbleToSelect, boolean isExpanding,
1078             boolean suppressAnimation, boolean addOverflowToo) {
1079         BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView();
1080         mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView(),
1081                 bubbleToSelectView, addOverflowToo ? () -> showOverflow(true) : null);
1082         addedBubble.getView().setOnClickListener(mBubbleClickListener);
1083         addedBubble.getView().setController(mBubbleViewController);
1084         removedBubble.getView().setController(null);
1085         mBubbleDragController.setupBubbleView(addedBubble.getView());
1086         if (!suppressAnimation) {
1087             animateBubbleNotification(addedBubble, isExpanding, /* isUpdate= */ false);
1088         }
1089     }
1090 
1091     /** Whether the overflow view is added to the bubble bar. */
1092     public boolean isOverflowAdded() {
1093         return mOverflowAdded;
1094     }
1095 
1096     /** Shows or hides the overflow view. */
1097     public void showOverflow(boolean showOverflow) {
1098         if (mOverflowAdded == showOverflow) return;
1099         mOverflowAdded = showOverflow;
1100         if (mOverflowAdded) {
1101             mBarView.addBubble(mOverflowBubble.getView());
1102             mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
1103             mOverflowBubble.getView().setController(mBubbleViewController);
1104         } else {
1105             mBarView.removeBubble(mOverflowBubble.getView());
1106             mOverflowBubble.getView().setOnClickListener(null);
1107             mOverflowBubble.getView().setController(null);
1108         }
1109     }
1110 
1111     /** Adds the overflow view to the bubble bar while animating a view away. */
1112     public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble,
1113             @Nullable BubbleBarBubble bubbleToSelect) {
1114         if (mOverflowAdded) return;
1115         mOverflowAdded = true;
1116         BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView();
1117         mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView(),
1118                 bubbleToSelectView, null /* onEndRunnable */);
1119         mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
1120         mOverflowBubble.getView().setController(mBubbleViewController);
1121         removedBubble.getView().setController(null);
1122     }
1123 
1124     /** Removes the overflow view to the bubble bar while animating a view in. */
1125     public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble,
1126             @Nullable BubbleBarBubble bubbleToSelect) {
1127         if (!mOverflowAdded) return;
1128         mOverflowAdded = false;
1129         BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView();
1130         mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView(),
1131                 bubbleToSelectView, null /* onEndRunnable */);
1132         addedBubble.getView().setOnClickListener(mBubbleClickListener);
1133         addedBubble.getView().setController(mBubbleViewController);
1134         mOverflowBubble.getView().setController(null);
1135     }
1136 
1137     /**
1138      * Adds the provided bubble to the bubble bar.
1139      */
1140     public void addBubble(BubbleBarItem b,
1141             boolean isExpanding,
1142             boolean suppressAnimation,
1143             @Nullable BubbleBarBubble bubbleToSelect
1144     ) {
1145         if (b != null) {
1146             BubbleView bubbleToSelectView =
1147                     bubbleToSelect == null ? null : bubbleToSelect.getView();
1148             mBarView.addBubble(b.getView(), bubbleToSelectView);
1149             b.getView().setOnClickListener(mBubbleClickListener);
1150             mBubbleDragController.setupBubbleView(b.getView());
1151             b.getView().setController(mBubbleViewController);
1152 
1153             if (suppressAnimation || !(b instanceof BubbleBarBubble bubble)) {
1154                 // the bubble bar and handle are initialized as part of the first bubble animation.
1155                 // if the animation is suppressed, immediately stash or show the bubble bar to
1156                 // ensure they've been initialized.
1157                 if (mTaskbarStashController.isInApp()
1158                         && mBubbleStashController.isTransientTaskBar()
1159                         && mTaskbarStashController.isStashed()) {
1160                     mBubbleStashController.stashBubbleBarImmediate();
1161                 } else {
1162                     mBubbleStashController.showBubbleBarImmediate();
1163                 }
1164                 return;
1165             }
1166             animateBubbleNotification(bubble, isExpanding, /* isUpdate= */ false);
1167         } else {
1168             Log.w(TAG, "addBubble, bubble was null!");
1169         }
1170     }
1171 
1172     /** Animates the bubble bar to notify the user about a bubble change. */
1173     public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding,
1174             boolean isUpdate) {
1175         // if we're not already animating another bubble, update the dot visibility. otherwise the
1176         // the dot will be handled as part of the animation.
1177         if (!mBubbleBarViewAnimator.isAnimating()) {
1178             bubble.getView().updateDotVisibility(
1179                     /* animate= */ !mBubbleStashController.isStashed());
1180         }
1181         // if we're expanded, don't animate the bubble bar.
1182         if (isExpanded()) {
1183             return;
1184         }
1185         boolean isInApp = mTaskbarStashController.isInApp();
1186         // if this is the first bubble, animate to the initial state.
1187         if (mBarView.getBubbleChildCount() == 1 && !isUpdate) {
1188             // If a drop target is visible and the first bubble is added, hide the empty drop target
1189             if (mBarView.isShowingDropTarget()) {
1190                 mBubbleBarPinController.hideDropTarget();
1191             }
1192             mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding,
1193                     mBarView.isShowingDropTarget());
1194             return;
1195         }
1196         // if we're not stashed or we're in persistent taskbar, animate for collapsed state.
1197         boolean animateForCollapsed = !mBubbleStashController.isStashed()
1198                 || !mBubbleStashController.isTransientTaskBar();
1199         if (animateForCollapsed) {
1200             mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble, isExpanding);
1201             return;
1202         }
1203 
1204         if (isInApp && mBubbleStashController.getHasHandleView()) {
1205             mBubbleBarViewAnimator.animateBubbleInForStashed(bubble, isExpanding);
1206         }
1207     }
1208 
1209     /**
1210      * Reorders the bubbles based on the provided list.
1211      */
1212     public void reorderBubbles(List<BubbleBarBubble> newOrder) {
1213         List<BubbleView> viewList = newOrder.stream().filter(Objects::nonNull)
1214                 .map(BubbleBarBubble::getView).toList();
1215         mBarView.reorder(viewList);
1216     }
1217 
1218     /**
1219      * Updates the selected bubble.
1220      */
1221     public void updateSelectedBubble(BubbleBarItem newlySelected) {
1222         mBarView.setSelectedBubble(newlySelected.getView());
1223     }
1224 
1225     /** @see #setExpanded(boolean, boolean) */
1226     public void setExpanded(boolean isExpanded) {
1227         setExpanded(isExpanded, /* maybeShowEdu= */ false);
1228     }
1229 
1230     /**
1231      * Sets whether the bubble bar should be expanded (not unstashed, but have the contents
1232      * within it expanded). This method notifies SystemUI that the bubble bar is expanded and
1233      * showing a selected bubble. This method should ONLY be called from UI events originating
1234      * from Launcher.
1235      *
1236      * @param isExpanded whether the bar should be expanded
1237      * @param maybeShowEdu whether we should show the edu view before expanding
1238      */
1239     public void setExpanded(boolean isExpanded, boolean maybeShowEdu) {
1240         // if we're trying to expand try showing the edu view instead
1241         if (maybeShowEdu && isExpanded && !mBarView.isExpanded() && maybeShowEduView()) {
1242             return;
1243         }
1244         if (!mBubbleBarPinning.isAnimating() && isExpanded != mBarView.isExpanded()) {
1245             mBarView.setExpanded(isExpanded);
1246             adjustTaskbarAndHotseatToBubbleBarState(isExpanded);
1247             if (!isExpanded) {
1248                 mSystemUiProxy.collapseBubbles();
1249             } else {
1250                 mBubbleBarController.showSelectedBubble();
1251                 mTaskbarStashController.updateAndAnimateTransientTaskbar(true /* stash */,
1252                         false /* shouldBubblesFollow */);
1253             }
1254         }
1255     }
1256 
1257     /**
1258      * Hides the persistent taskbar if it is going to intersect with the expanded bubble bar if in
1259      * app or overview.
1260      */
1261     private void adjustTaskbarAndHotseatToBubbleBarState(boolean isBubbleBarExpanded) {
1262         if (!mBubbleStashController.isBubblesShowingOnHome()
1263                 && !mBubbleStashController.isTransientTaskBar()) {
1264             boolean hideTaskbar = isBubbleBarExpanded && isIntersectingTaskbar();
1265             Animator taskbarAlphaAnimator = mTaskbarViewPropertiesProvider.getIconsAlpha()
1266                     .animateToValue(hideTaskbar ? 0 : 1);
1267             taskbarAlphaAnimator.setDuration(hideTaskbar
1268                     ? TASKBAR_FADE_OUT_DURATION_MS : TASKBAR_FADE_IN_DURATION_MS);
1269             if (!hideTaskbar) {
1270                 taskbarAlphaAnimator.setStartDelay(TASKBAR_FADE_IN_DELAY_MS);
1271             }
1272             taskbarAlphaAnimator.setInterpolator(Interpolators.LINEAR);
1273             taskbarAlphaAnimator.start();
1274         }
1275     }
1276 
1277     /** Return {@code true} if expanded bubble bar would intersect the taskbar. */
1278     public boolean isIntersectingTaskbar() {
1279         if (mBarView.isExpanding() || mBarView.isExpanded()) {
1280             Rect taskbarViewBounds = mTaskbarViewPropertiesProvider.getTaskbarViewBounds();
1281             return mBarView.getBubbleBarExpandedBounds().intersect(taskbarViewBounds);
1282         } else {
1283             return false;
1284         }
1285     }
1286 
1287     /**
1288      * Sets whether the bubble bar should be expanded. This method is used in response to UI events
1289      * from SystemUI.
1290      */
1291     public void setExpandedFromSysui(boolean isExpanded) {
1292         if (isNewBubbleAnimationRunningOrPending() && isExpanded) {
1293             mBubbleBarViewAnimator.expandedWhileAnimating();
1294             return;
1295         }
1296         if (!isExpanded) {
1297             mBubbleStashController.stashBubbleBar();
1298         } else {
1299             mBubbleStashController.showBubbleBar(true /* expand the bubbles */);
1300         }
1301     }
1302 
1303     /**
1304      * Stores a request to show the education view for later processing when appropriate.
1305      *
1306      * @see #maybeShowEduView()
1307      */
1308     public void prepareToShowEducation() {
1309         mShouldShowEducation = true;
1310     }
1311 
1312     /**
1313      * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI
1314      * that a bubble is being dragged to dismiss.
1315      *
1316      * @param bubbleView dragged bubble view
1317      */
1318     public void onBubbleDragStart(@NonNull BubbleView bubbleView) {
1319         if (bubbleView.getBubble() == null) return;
1320 
1321         mSystemUiProxy.startBubbleDrag(bubbleView.getBubble().getKey());
1322         mBarView.setDraggedBubble(bubbleView);
1323     }
1324 
1325     /**
1326      * Notifies SystemUI to expand the selected bubble when the bubble is released.
1327      */
1328     public void onBubbleDragRelease(BubbleBarLocation location) {
1329         mSystemUiProxy.stopBubbleDrag(location, mBarView.getRestingTopPositionOnScreen());
1330     }
1331 
1332     /** Handle given bubble being dismissed */
1333     public void onBubbleDismissed(BubbleView bubble) {
1334         mBubbleBarController.onBubbleDismissed(bubble);
1335         mBarView.removeBubble(bubble);
1336     }
1337 
1338     /**
1339      * Notifies {@link BubbleBarView} that drag and all animations are finished.
1340      */
1341     public void onBubbleDragEnd() {
1342         mBarView.setDraggedBubble(null);
1343     }
1344 
1345     /** Notifies that dragging the bubble bar ended. */
1346     public void onBubbleBarDragEnd() {
1347         // we may have changed the bubble bar translation Y value from the value it had at the
1348         // beginning of the drag, so update the translation Y animator state
1349         mBubbleBarTranslationY.updateValue(mBarView.getTranslationY());
1350     }
1351 
1352     /**
1353      * Get translation for bubble bar when drag is released.
1354      *
1355      * @see BubbleBarView#getBubbleBarDragReleaseTranslation(PointF, BubbleBarLocation)
1356      */
1357     public PointF getBubbleBarDragReleaseTranslation(PointF initialTranslation,
1358             BubbleBarLocation location) {
1359         return mBarView.getBubbleBarDragReleaseTranslation(initialTranslation, location);
1360     }
1361 
1362     /**
1363      * Get translation for bubble view when drag is released.
1364      *
1365      * @see BubbleBarView#getDraggedBubbleReleaseTranslation(PointF, BubbleBarLocation)
1366      */
1367     public PointF getDraggedBubbleReleaseTranslation(PointF initialTranslation,
1368             BubbleBarLocation location) {
1369         if (location == mBarView.getBubbleBarLocation()) {
1370             return initialTranslation;
1371         }
1372         return mBarView.getDraggedBubbleReleaseTranslation(initialTranslation, location);
1373     }
1374 
1375     /**
1376      * Notify SystemUI that the given bubble has been dismissed.
1377      */
1378     public void notifySysUiBubbleDismissed(@NonNull BubbleBarItem bubble) {
1379         mSystemUiProxy.dragBubbleToDismiss(bubble.getKey(), mTimeSource.currentTimeMillis());
1380     }
1381 
1382     /**
1383      * Called when bubble stack was dismissed
1384      */
1385     public void onDismissAllBubbles() {
1386         mSystemUiProxy.removeAllBubbles();
1387     }
1388 
1389     /** Removes all existing bubble views */
1390     public void removeAllBubbles() {
1391         mOverflowAdded = false;
1392         mBarView.removeAllViews();
1393     }
1394 
1395     /** Returns the view index of the existing bubble */
1396     public int bubbleViewIndex(View bubbleView) {
1397         return mBarView.indexOfChild(bubbleView);
1398     }
1399 
1400     /**
1401      * Set listener to be notified when bubble bar bounds have changed
1402      */
1403     public void setBoundsChangeListener(@Nullable BubbleBarBoundsChangeListener listener) {
1404         mBoundsChangeListener = listener;
1405     }
1406 
1407     /** Called when the controller is destroyed. */
1408     public void onDestroy() {
1409         adjustTaskbarAndHotseatToBubbleBarState(/*isBubbleBarExpanded = */false);
1410     }
1411 
1412     /**
1413      * Removes the bubble from the bubble bar and notifies sysui that the bubble should move to
1414      * full screen.
1415      */
1416     public void moveDraggedBubbleToFullscreen(@NonNull BubbleView bubbleView, Point dropLocation) {
1417         if (bubbleView.getBubble() == null) {
1418             return;
1419         }
1420         String key = bubbleView.getBubble().getKey();
1421         mSystemUiProxy.moveDraggedBubbleToFullscreen(key, dropLocation);
1422         onBubbleDismissed(bubbleView);
1423     }
1424 
1425     /**
1426      * Create an animator for showing or hiding bubbles when stashed state changes
1427      *
1428      * @param isStashed {@code true} when bubble bar should be stashed to the handle
1429      */
1430     public Animator createRevealAnimatorForStashChange(boolean isStashed) {
1431         Rect stashedHandleBounds = new Rect();
1432         mBubbleStashController.getHandleBounds(stashedHandleBounds);
1433         int childCount = mBarView.getChildCount();
1434         float newChildWidth = (float) stashedHandleBounds.width() / childCount;
1435         AnimatorSet animatorSet = new AnimatorSet();
1436         for (int i = 0; i < childCount; i++) {
1437             BubbleView child = (BubbleView) mBarView.getChildAt(i);
1438             animatorSet.play(
1439                     createRevealAnimForBubble(child, isStashed, stashedHandleBounds,
1440                             newChildWidth));
1441         }
1442         return animatorSet;
1443     }
1444 
1445     private Animator createRevealAnimForBubble(BubbleView bubbleView, boolean isStashed,
1446             Rect stashedHandleBounds, float newWidth) {
1447         Rect viewBounds = new Rect(0, 0, bubbleView.getWidth(), bubbleView.getHeight());
1448 
1449         int viewCenterY = viewBounds.centerY();
1450         int halfHandleHeight = stashedHandleBounds.height() / 2;
1451         int widthDelta = Math.max(0, (int) (viewBounds.width() - newWidth) / 2);
1452 
1453         Rect stashedViewBounds = new Rect(
1454                 viewBounds.left + widthDelta,
1455                 viewCenterY - halfHandleHeight,
1456                 viewBounds.right - widthDelta,
1457                 viewCenterY + halfHandleHeight
1458         );
1459 
1460         float viewRadius = 0f; // Use 0 to not clip the new message dot or the app icon
1461         float stashedRadius = stashedViewBounds.height() / 2f;
1462 
1463         return new RoundedRectRevealOutlineProvider(viewRadius, stashedRadius, viewBounds,
1464                 stashedViewBounds).createRevealAnimator(bubbleView, !isStashed, 0);
1465     }
1466 
1467     /**
1468      * Listener to receive updates about bubble bar bounds changing
1469      */
1470     public interface BubbleBarBoundsChangeListener {
1471         /** Called when bounds have changed */
1472         void onBoundsChanged();
1473     }
1474 
1475     /** Interface for getting the current timestamp. */
1476     interface TimeSource {
1477         long currentTimeMillis();
1478     }
1479 
1480     /** Dumps the state of BubbleBarViewController. */
1481     public void dump(PrintWriter pw) {
1482         pw.println("Bubble bar view controller state:");
1483         pw.println("  mHiddenForSysui: " + mHiddenForSysui);
1484         pw.println("  mHiddenForNoBubbles: " + mHiddenForNoBubbles);
1485         pw.println("  mHiddenForStashed: " + mHiddenForStashed);
1486         pw.println("  mShouldShowEducation: " + mShouldShowEducation);
1487         pw.println("  mBubbleBarTranslationY.value: " + mBubbleBarTranslationY.value);
1488         pw.println("  mBubbleBarSwipeUpTranslationY: " + mBubbleBarSwipeUpTranslationY);
1489         pw.println("  mOverflowAdded: " + mOverflowAdded);
1490         if (mBarView != null) {
1491             mBarView.dump(pw);
1492         } else {
1493             pw.println("  Bubble bar view is null!");
1494         }
1495     }
1496 
1497     /** Interface for BubbleBarViewController to get the taskbar view properties. */
1498     public interface TaskbarViewPropertiesProvider {
1499 
1500         /** Returns the bounds of the taskbar. */
1501         Rect getTaskbarViewBounds();
1502 
1503         /** Returns taskbar icons alpha */
1504         MultiPropertyFactory<View>.MultiProperty getIconsAlpha();
1505     }
1506 }
1507