blob: d9b9ae2ad5bb211c003034d0dc477e615093500d [file] [log] [blame]
Eric Erfanian938468d2017-10-24 14:05:52 -07001/*
2 * Copyright (C) 2017 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
17package com.android.newbubble;
18
19import android.animation.Animator;
20import android.animation.AnimatorSet;
21import android.animation.ObjectAnimator;
22import android.annotation.SuppressLint;
23import android.app.PendingIntent.CanceledException;
24import android.content.Context;
25import android.content.Intent;
26import android.graphics.PixelFormat;
27import android.graphics.drawable.Animatable;
28import android.graphics.drawable.Drawable;
29import android.graphics.drawable.RippleDrawable;
30import android.net.Uri;
31import android.os.Handler;
32import android.provider.Settings;
33import android.support.annotation.ColorInt;
34import android.support.annotation.IntDef;
35import android.support.annotation.NonNull;
36import android.support.annotation.Nullable;
37import android.support.annotation.VisibleForTesting;
38import android.support.v4.graphics.ColorUtils;
39import android.support.v4.graphics.drawable.DrawableCompat;
40import android.support.v4.os.BuildCompat;
41import android.support.v4.view.animation.FastOutLinearInInterpolator;
42import android.support.v4.view.animation.LinearOutSlowInInterpolator;
43import android.transition.TransitionManager;
44import android.transition.TransitionValues;
45import android.view.ContextThemeWrapper;
46import android.view.Gravity;
47import android.view.LayoutInflater;
48import android.view.MotionEvent;
49import android.view.View;
50import android.view.ViewGroup;
51import android.view.ViewPropertyAnimator;
52import android.view.ViewTreeObserver.OnPreDrawListener;
53import android.view.WindowManager;
54import android.view.WindowManager.LayoutParams;
55import android.view.animation.AnticipateInterpolator;
56import android.view.animation.OvershootInterpolator;
57import android.widget.ImageView;
58import android.widget.TextView;
59import android.widget.ViewAnimator;
60import com.android.newbubble.NewBubbleInfo.Action;
61import java.lang.annotation.Retention;
62import java.lang.annotation.RetentionPolicy;
63import java.util.List;
64
65/**
66 * Creates and manages a bubble window from information in a {@link NewBubbleInfo}. Before creating,
67 * be sure to check whether bubbles may be shown using {@link #canShowBubbles(Context)} and request
68 * permission if necessary ({@link #getRequestPermissionIntent(Context)} is provided for
69 * convenience)
70 */
71public class NewBubble {
72 // This class has some odd behavior that is not immediately obvious in order to avoid jank when
73 // resizing. See http://go/bubble-resize for details.
74
75 // How long text should show after showText(CharSequence) is called
76 private static final int SHOW_TEXT_DURATION_MILLIS = 3000;
77 // How long the new window should show before destroying the old one during resize operations.
78 // This ensures the new window has had time to draw first.
79 private static final int WINDOW_REDRAW_DELAY_MILLIS = 50;
80
81 private static Boolean canShowBubblesForTesting = null;
82
83 private final Context context;
84 private final WindowManager windowManager;
85
86 private final Handler handler;
87 private LayoutParams windowParams;
88
89 // Initialized in factory method
90 @SuppressWarnings("NullableProblems")
91 @NonNull
92 private NewBubbleInfo currentInfo;
93
94 @Visibility private int visibility;
95 private boolean expanded;
96 private boolean textShowing;
97 private boolean hideAfterText;
98 private CharSequence textAfterShow;
99 private int collapseEndAction;
100
101 @VisibleForTesting ViewHolder viewHolder;
102 private ViewPropertyAnimator collapseAnimation;
103 private Integer overrideGravity;
104 private ViewPropertyAnimator exitAnimator;
105
106 private final Runnable collapseRunnable =
107 new Runnable() {
108 @Override
109 public void run() {
110 textShowing = false;
111 if (hideAfterText) {
112 // Always reset here since text shouldn't keep showing.
113 hideAndReset();
114 } else {
115 doResize(
116 () -> viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_ICON));
117 }
118 }
119 };
120
121 private BubbleExpansionStateListener bubbleExpansionStateListener;
122
123 /** Type of action after bubble collapse */
124 @Retention(RetentionPolicy.SOURCE)
125 @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE})
126 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
127 public @interface CollapseEnd {
128 int NOTHING = 0;
129 int HIDE = 1;
130 }
131
132 @Retention(RetentionPolicy.SOURCE)
133 @IntDef({Visibility.ENTERING, Visibility.SHOWING, Visibility.EXITING, Visibility.HIDDEN})
134 private @interface Visibility {
135 int HIDDEN = 0;
136 int ENTERING = 1;
137 int SHOWING = 2;
138 int EXITING = 3;
139 }
140
141 /** Indicate bubble expansion state. */
142 @Retention(RetentionPolicy.SOURCE)
143 @IntDef({ExpansionState.START_EXPANDING, ExpansionState.START_COLLAPSING})
144 public @interface ExpansionState {
145 // TODO(yueg): add more states when needed
146 int START_EXPANDING = 0;
147 int START_COLLAPSING = 1;
148 }
149
150 /**
151 * Determines whether bubbles can be shown based on permissions obtained. This should be checked
152 * before attempting to create a Bubble.
153 *
154 * @return true iff bubbles are able to be shown.
155 * @see Settings#canDrawOverlays(Context)
156 */
157 public static boolean canShowBubbles(@NonNull Context context) {
158 return canShowBubblesForTesting != null
159 ? canShowBubblesForTesting
160 : Settings.canDrawOverlays(context);
161 }
162
163 @VisibleForTesting(otherwise = VisibleForTesting.NONE)
164 public static void setCanShowBubblesForTesting(boolean canShowBubbles) {
165 canShowBubblesForTesting = canShowBubbles;
166 }
167
168 /** Returns an Intent to request permission to show overlays */
169 @NonNull
170 public static Intent getRequestPermissionIntent(@NonNull Context context) {
171 return new Intent(
172 Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
173 Uri.fromParts("package", context.getPackageName(), null));
174 }
175
176 /** Creates instances of Bubble. The default implementation just calls the constructor. */
177 @VisibleForTesting
178 public interface BubbleFactory {
179 NewBubble createBubble(@NonNull Context context, @NonNull Handler handler);
180 }
181
182 private static BubbleFactory bubbleFactory = NewBubble::new;
183
184 public static NewBubble createBubble(@NonNull Context context, @NonNull NewBubbleInfo info) {
185 NewBubble bubble = bubbleFactory.createBubble(context, new Handler());
186 bubble.setBubbleInfo(info);
187 return bubble;
188 }
189
190 @VisibleForTesting
191 public static void setBubbleFactory(@NonNull BubbleFactory bubbleFactory) {
192 NewBubble.bubbleFactory = bubbleFactory;
193 }
194
195 @VisibleForTesting
196 public static void resetBubbleFactory() {
197 NewBubble.bubbleFactory = NewBubble::new;
198 }
199
200 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
201 NewBubble(@NonNull Context context, @NonNull Handler handler) {
202 context = new ContextThemeWrapper(context, R.style.Theme_AppCompat);
203 this.context = context;
204 this.handler = handler;
205 windowManager = context.getSystemService(WindowManager.class);
206
207 viewHolder = new ViewHolder(context);
208 }
209
210 /** Expands the main bubble menu. */
211 public void expand(boolean isUserAction) {
212 if (bubbleExpansionStateListener != null) {
213 bubbleExpansionStateListener.onBubbleExpansionStateChanged(
214 ExpansionState.START_EXPANDING, isUserAction);
215 }
216 doResize(() -> viewHolder.setDrawerVisibility(View.VISIBLE));
217 View expandedView = viewHolder.getExpandedView();
218 expandedView
219 .getViewTreeObserver()
220 .addOnPreDrawListener(
221 new OnPreDrawListener() {
222 @Override
223 public boolean onPreDraw() {
224 // Animate expanded view to move from above primary button to its final position
225 expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
226 expandedView.setTranslationY(-viewHolder.getRoot().getHeight());
227 expandedView
228 .animate()
229 .setInterpolator(new LinearOutSlowInInterpolator())
230 .translationY(0);
231 return false;
232 }
233 });
234 setFocused(true);
235 expanded = true;
236 }
237
238 /**
239 * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is
240 * already showing this method does nothing.
241 */
242 public void show() {
243 if (collapseEndAction == CollapseEnd.HIDE) {
244 // If show() was called while collapsing, make sure we don't hide after.
245 collapseEndAction = CollapseEnd.NOTHING;
246 }
247 if (visibility == Visibility.SHOWING || visibility == Visibility.ENTERING) {
248 return;
249 }
250
251 hideAfterText = false;
252
253 if (windowParams == null) {
254 // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O.
255 @SuppressWarnings("deprecation")
256 @SuppressLint("InlinedApi")
257 int type =
258 BuildCompat.isAtLeastO()
259 ? LayoutParams.TYPE_APPLICATION_OVERLAY
260 : LayoutParams.TYPE_PHONE;
261
262 windowParams =
263 new LayoutParams(
264 type,
265 LayoutParams.FLAG_NOT_TOUCH_MODAL
266 | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
267 | LayoutParams.FLAG_NOT_FOCUSABLE
268 | LayoutParams.FLAG_LAYOUT_NO_LIMITS,
269 PixelFormat.TRANSLUCENT);
270 windowParams.gravity = Gravity.TOP | Gravity.LEFT;
271 windowParams.x =
272 context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_horizontal);
273 windowParams.y = currentInfo.getStartingYPosition();
274 windowParams.height = LayoutParams.WRAP_CONTENT;
275 windowParams.width = LayoutParams.WRAP_CONTENT;
276 }
277
278 if (exitAnimator != null) {
279 exitAnimator.cancel();
280 exitAnimator = null;
281 } else {
282 windowManager.addView(viewHolder.getRoot(), windowParams);
283 viewHolder.getPrimaryButton().setScaleX(0);
284 viewHolder.getPrimaryButton().setScaleY(0);
285 }
286
287 viewHolder.setChildClickable(true);
288 visibility = Visibility.ENTERING;
289 viewHolder
290 .getPrimaryButton()
291 .animate()
292 .setInterpolator(new OvershootInterpolator())
293 .scaleX(1)
294 .scaleY(1)
295 .withEndAction(
296 () -> {
297 visibility = Visibility.SHOWING;
298 // Show the queued up text, if available.
299 if (textAfterShow != null) {
300 showText(textAfterShow);
301 textAfterShow = null;
302 }
303 })
304 .start();
305
306 updatePrimaryIconAnimation();
307 }
308
309 /** Hide the bubble. */
310 public void hide() {
311 if (hideAfterText) {
312 // hideAndReset() will be called after showing text, do nothing here.
313 return;
314 }
315 hideHelper(this::defaultAfterHidingAnimation);
316 }
317
318 /** Hide the bubble and reset {@viewHolder} to initial state */
319 public void hideAndReset() {
320 hideHelper(
321 () -> {
322 defaultAfterHidingAnimation();
323 reset();
324 });
325 }
326
327 /** Returns whether the bubble is currently visible */
328 public boolean isVisible() {
329 return visibility == Visibility.SHOWING
330 || visibility == Visibility.ENTERING
331 || visibility == Visibility.EXITING;
332 }
333
334 /**
335 * Set the info for this Bubble to display
336 *
337 * @param bubbleInfo the BubbleInfo to display in this Bubble.
338 */
339 public void setBubbleInfo(@NonNull NewBubbleInfo bubbleInfo) {
340 currentInfo = bubbleInfo;
341 update();
342 }
343
344 /**
345 * Update the state and behavior of actions.
346 *
347 * @param actions the new state of the bubble's actions
348 */
349 public void updateActions(@NonNull List<Action> actions) {
350 currentInfo = NewBubbleInfo.from(currentInfo).setActions(actions).build();
351 updateButtonStates();
352 }
353
354 /** Returns the currently displayed NewBubbleInfo */
355 public NewBubbleInfo getBubbleInfo() {
356 return currentInfo;
357 }
358
359 /**
360 * Display text in the main bubble. The bubble's drawer is not expandable while text is showing,
361 * and the drawer will be closed if already open.
362 *
363 * @param text the text to display to the user
364 */
365 public void showText(@NonNull CharSequence text) {
366 textShowing = true;
367 if (expanded) {
368 startCollapse(CollapseEnd.NOTHING, false);
369 doShowText(text);
370 } else {
371 // Need to transition from old bounds to new bounds manually
372 NewChangeOnScreenBounds transition = new NewChangeOnScreenBounds();
373 // Prepare and capture start values
374 TransitionValues startValues = new TransitionValues();
375 startValues.view = viewHolder.getPrimaryButton();
376 transition.addTarget(startValues.view);
377 transition.captureStartValues(startValues);
378
379 // If our view is not laid out yet, postpone showing the text.
380 if (startValues.values.isEmpty()) {
381 textAfterShow = text;
382 return;
383 }
384
385 doResize(
386 () -> {
387 doShowText(text);
388 // Hide the text so we can animate it in
389 viewHolder.getPrimaryText().setAlpha(0);
390
391 ViewAnimator primaryButton = viewHolder.getPrimaryButton();
392 // Cancel the automatic transition scheduled in doShowText
393 TransitionManager.endTransitions((ViewGroup) primaryButton.getParent());
394 primaryButton
395 .getViewTreeObserver()
396 .addOnPreDrawListener(
397 new OnPreDrawListener() {
398 @Override
399 public boolean onPreDraw() {
400 primaryButton.getViewTreeObserver().removeOnPreDrawListener(this);
401
402 // Prepare and capture end values, always use the size of primaryText since
403 // its invisibility makes primaryButton smaller than expected
404 TransitionValues endValues = new TransitionValues();
405 endValues.values.put(
406 NewChangeOnScreenBounds.PROPNAME_WIDTH,
407 viewHolder.getPrimaryText().getWidth());
408 endValues.values.put(
409 NewChangeOnScreenBounds.PROPNAME_HEIGHT,
410 viewHolder.getPrimaryText().getHeight());
411 endValues.view = primaryButton;
412 transition.addTarget(endValues.view);
413 transition.captureEndValues(endValues);
414
415 // animate the primary button bounds change
416 Animator bounds =
417 transition.createAnimator(primaryButton, startValues, endValues);
418
419 // Animate the text in
420 Animator alpha =
421 ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f);
422
423 AnimatorSet set = new AnimatorSet();
424 set.play(bounds).before(alpha);
425 set.start();
426 return false;
427 }
428 });
429 });
430 }
431 handler.removeCallbacks(collapseRunnable);
432 handler.postDelayed(collapseRunnable, SHOW_TEXT_DURATION_MILLIS);
433 }
434
435 public void setBubbleExpansionStateListener(
436 BubbleExpansionStateListener bubbleExpansionStateListener) {
437 this.bubbleExpansionStateListener = bubbleExpansionStateListener;
438 }
439
440 @Nullable
441 Integer getGravityOverride() {
442 return overrideGravity;
443 }
444
445 void onMoveStart() {
446 startCollapse(CollapseEnd.NOTHING, true);
447 viewHolder
448 .getPrimaryButton()
449 .animate()
450 .translationZ(
451 context.getResources().getDimensionPixelOffset(R.dimen.bubble_move_elevation_change));
452 }
453
454 void onMoveFinish() {
455 viewHolder.getPrimaryButton().animate().translationZ(0);
456 // If it's GONE, no resize is necessary. If it's VISIBLE, it will get cleaned up when the
457 // collapse animation finishes
458 if (viewHolder.getExpandedView().getVisibility() == View.INVISIBLE) {
459 doResize(null);
460 }
461 }
462
463 void primaryButtonClick() {
464 if (textShowing || currentInfo.getActions().isEmpty()) {
465 return;
466 }
467 if (expanded) {
468 startCollapse(CollapseEnd.NOTHING, true);
469 } else {
470 expand(true);
471 }
472 }
473
474 LayoutParams getWindowParams() {
475 return windowParams;
476 }
477
478 View getRootView() {
479 return viewHolder.getRoot();
480 }
481
482 /**
483 * Hide the bubble if visible. Will run a short exit animation and before hiding, and {@code
484 * afterHiding} after hiding. If the bubble is currently showing text, will hide after the text is
485 * done displaying. If the bubble is not visible this method does nothing.
486 */
487 private void hideHelper(Runnable afterHiding) {
488 if (visibility == Visibility.HIDDEN || visibility == Visibility.EXITING) {
489 return;
490 }
491
492 // Make bubble non clickable to prevent further buggy actions
493 viewHolder.setChildClickable(false);
494
495 if (textShowing) {
496 hideAfterText = true;
497 return;
498 }
499
500 if (collapseAnimation != null) {
501 collapseEndAction = CollapseEnd.HIDE;
502 return;
503 }
504
505 if (expanded) {
506 startCollapse(CollapseEnd.HIDE, false);
507 return;
508 }
509
510 visibility = Visibility.EXITING;
511 exitAnimator =
512 viewHolder
513 .getPrimaryButton()
514 .animate()
515 .setInterpolator(new AnticipateInterpolator())
516 .scaleX(0)
517 .scaleY(0)
518 .withEndAction(afterHiding);
519 exitAnimator.start();
520 }
521
522 private void reset() {
523 viewHolder = new ViewHolder(viewHolder.getRoot().getContext());
524 update();
525 }
526
527 private void update() {
528 RippleDrawable backgroundRipple =
529 (RippleDrawable)
530 context.getResources().getDrawable(R.drawable.bubble_ripple_circle, context.getTheme());
531 int primaryTint =
532 ColorUtils.compositeColors(
533 context.getColor(R.color.bubble_primary_background_darken),
534 currentInfo.getPrimaryColor());
535 backgroundRipple.getDrawable(0).setTint(primaryTint);
536 viewHolder.getPrimaryButton().setBackground(backgroundRipple);
537
538 viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon());
539 updatePrimaryIconAnimation();
540
541 updateButtonStates();
542 }
543
544 private void updatePrimaryIconAnimation() {
545 Drawable drawable = viewHolder.getPrimaryIcon().getDrawable();
546 if (drawable instanceof Animatable) {
547 if (isVisible()) {
548 ((Animatable) drawable).start();
549 } else {
550 ((Animatable) drawable).stop();
551 }
552 }
553 }
554
555 private void updateButtonStates() {
556 int colorBlue = context.getColor(R.color.bubble_button_text_color_blue);
557 int colorWhite = context.getColor(R.color.bubble_button_text_color_white);
558
559 configureButton(currentInfo.getActions().get(0), viewHolder.getFullScreenButton(), colorBlue);
560 configureButton(currentInfo.getActions().get(1), viewHolder.getMuteButton(), colorBlue);
561 configureButton(currentInfo.getActions().get(2), viewHolder.getAudioRouteButton(), colorBlue);
562 configureButton(currentInfo.getActions().get(3), viewHolder.getEndCallButton(), colorWhite);
563 }
564
565 private void doShowText(@NonNull CharSequence text) {
566 TransitionManager.beginDelayedTransition((ViewGroup) viewHolder.getPrimaryButton().getParent());
567 viewHolder.getPrimaryText().setText(text);
568 viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_TEXT);
569 }
570
571 private void configureButton(Action action, NewCheckableButton button, @ColorInt int iconColor) {
572 Drawable iconDrawable = DrawableCompat.wrap(action.getIconDrawable());
573 DrawableCompat.setTint(iconDrawable.mutate(), iconColor);
574
575 button.setCompoundDrawablesWithIntrinsicBounds(iconDrawable, null, null, null);
576 button.setChecked(action.isChecked());
577 button.setEnabled(action.isEnabled());
578 if (action.getName() != null) {
579 button.setText(action.getName());
580 }
581 button.setOnClickListener(v -> doAction(action));
582 }
583
584 private void doAction(Action action) {
585 try {
586 action.getIntent().send();
587 } catch (CanceledException e) {
588 throw new RuntimeException(e);
589 }
590 }
591
592 private void doResize(@Nullable Runnable operation) {
593 // If we're resizing on the right side of the screen, there is an implicit move operation
594 // necessary. The WindowManager does not sync the move and resize operations, so serious jank
595 // would occur. To fix this, instead of resizing the window, we create a new one and destroy
596 // the old one. There is a short delay before destroying the old view to ensure the new one has
597 // had time to draw.
598 ViewHolder oldViewHolder = viewHolder;
599 if (isDrawingFromRight()) {
600 viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext());
601 update();
602 viewHolder
603 .getPrimaryButton()
604 .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild());
605 viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText());
606 }
607
608 if (operation != null) {
609 operation.run();
610 }
611
612 if (isDrawingFromRight()) {
613 swapViewHolders(oldViewHolder);
614 }
615 }
616
617 private void swapViewHolders(ViewHolder oldViewHolder) {
618 ViewGroup root = viewHolder.getRoot();
619 windowManager.addView(root, windowParams);
620 root.getViewTreeObserver()
621 .addOnPreDrawListener(
622 new OnPreDrawListener() {
623 @Override
624 public boolean onPreDraw() {
625 root.getViewTreeObserver().removeOnPreDrawListener(this);
626 // Wait a bit before removing the old view; make sure the new one has drawn over it.
627 handler.postDelayed(
628 () -> windowManager.removeView(oldViewHolder.getRoot()),
629 WINDOW_REDRAW_DELAY_MILLIS);
630 return true;
631 }
632 });
633 }
634
635 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
636 public void startCollapse(@CollapseEnd int endAction, boolean isUserAction) {
637 View expandedView = viewHolder.getExpandedView();
638 if (expandedView.getVisibility() != View.VISIBLE || collapseAnimation != null) {
639 // Drawer is already collapsed or animation is running.
640 return;
641 }
642
643 overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT;
644 setFocused(false);
645
646 if (collapseEndAction == CollapseEnd.NOTHING) {
647 collapseEndAction = endAction;
648 }
649 if (bubbleExpansionStateListener != null && collapseEndAction == CollapseEnd.NOTHING) {
650 bubbleExpansionStateListener.onBubbleExpansionStateChanged(
651 ExpansionState.START_COLLAPSING, isUserAction);
652 }
653 // Animate expanded view to move from its position to above primary button and hide
654 collapseAnimation =
655 expandedView
656 .animate()
657 .translationY(-viewHolder.getRoot().getHeight())
658 .setInterpolator(new FastOutLinearInInterpolator())
659 .withEndAction(
660 () -> {
661 collapseAnimation = null;
662 expanded = false;
663
664 if (textShowing) {
665 // Will do resize once the text is done.
666 return;
667 }
668
669 // Hide the drawer and resize if possible.
670 viewHolder.setDrawerVisibility(View.INVISIBLE);
671 if (!viewHolder.isMoving() || !isDrawingFromRight()) {
672 doResize(() -> viewHolder.setDrawerVisibility(View.GONE));
673 }
674
675 // If this collapse was to come before a hide, do it now.
676 if (collapseEndAction == CollapseEnd.HIDE) {
677 hide();
678 }
679 collapseEndAction = CollapseEnd.NOTHING;
680
681 // Resume normal gravity after any resizing is done.
682 handler.postDelayed(
683 () -> {
684 overrideGravity = null;
685 if (!viewHolder.isMoving()) {
686 viewHolder.undoGravityOverride();
687 }
688 },
689 // Need to wait twice as long for resize and layout
690 WINDOW_REDRAW_DELAY_MILLIS * 2);
691 });
692 }
693
694 private boolean isDrawingFromRight() {
695 return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
696 }
697
698 private void setFocused(boolean focused) {
699 if (focused) {
700 windowParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
701 } else {
702 windowParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
703 }
704 windowManager.updateViewLayout(getRootView(), windowParams);
705 }
706
707 private void defaultAfterHidingAnimation() {
708 exitAnimator = null;
709 windowManager.removeView(viewHolder.getRoot());
710 visibility = Visibility.HIDDEN;
711
712 updatePrimaryIconAnimation();
713 }
714
715 @VisibleForTesting
716 class ViewHolder {
717
718 public static final int CHILD_INDEX_ICON = 0;
719 public static final int CHILD_INDEX_TEXT = 1;
720
721 private final NewMoveHandler moveHandler;
722 private final NewWindowRoot root;
723 private final ViewAnimator primaryButton;
724 private final ImageView primaryIcon;
725 private final TextView primaryText;
726
727 private final NewCheckableButton fullScreenButton;
728 private final NewCheckableButton muteButton;
729 private final NewCheckableButton audioRouteButton;
730 private final NewCheckableButton endCallButton;
731 private final View expandedView;
732
733 public ViewHolder(Context context) {
734 // Window root is not in the layout file so that the inflater has a view to inflate into
735 this.root = new NewWindowRoot(context);
736 LayoutInflater inflater = LayoutInflater.from(root.getContext());
737 View contentView = inflater.inflate(R.layout.new_bubble_base, root, true);
738 expandedView = contentView.findViewById(R.id.bubble_expanded_layout);
739 primaryButton = contentView.findViewById(R.id.bubble_button_primary);
740 primaryIcon = contentView.findViewById(R.id.bubble_icon_primary);
741 primaryText = contentView.findViewById(R.id.bubble_text);
742
743 fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen);
744 muteButton = contentView.findViewById(R.id.bubble_button_mute);
745 audioRouteButton = contentView.findViewById(R.id.bubble_button_audio_route);
746 endCallButton = contentView.findViewById(R.id.bubble_button_end_call);
747
748 root.setOnBackPressedListener(
749 () -> {
750 if (visibility == Visibility.SHOWING && expanded) {
751 startCollapse(CollapseEnd.NOTHING, true);
752 return true;
753 }
754 return false;
755 });
756 root.setOnConfigurationChangedListener(
757 (configuration) -> {
758 // The values in the current MoveHandler may be stale, so replace it. Then ensure the
759 // Window is in bounds
760 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
761 moveHandler.snapToBounds();
762 });
763 root.setOnTouchListener(
764 (v, event) -> {
765 if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
766 startCollapse(CollapseEnd.NOTHING, true);
767 return true;
768 }
769 return false;
770 });
771 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
772 }
773
774 private void setChildClickable(boolean clickable) {
775 fullScreenButton.setClickable(clickable);
776 muteButton.setClickable(clickable);
777 audioRouteButton.setClickable(clickable);
778 endCallButton.setClickable(clickable);
779
780 // For primaryButton
781 moveHandler.setClickable(clickable);
782 }
783
784 public ViewGroup getRoot() {
785 return root;
786 }
787
788 public ViewAnimator getPrimaryButton() {
789 return primaryButton;
790 }
791
792 public ImageView getPrimaryIcon() {
793 return primaryIcon;
794 }
795
796 public TextView getPrimaryText() {
797 return primaryText;
798 }
799
800 public View getExpandedView() {
801 return expandedView;
802 }
803
804 public NewCheckableButton getFullScreenButton() {
805 return fullScreenButton;
806 }
807
808 public NewCheckableButton getMuteButton() {
809 return muteButton;
810 }
811
812 public NewCheckableButton getAudioRouteButton() {
813 return audioRouteButton;
814 }
815
816 public NewCheckableButton getEndCallButton() {
817 return endCallButton;
818 }
819
820 public void setDrawerVisibility(int visibility) {
821 expandedView.setVisibility(visibility);
822 }
823
824 public boolean isMoving() {
825 return moveHandler.isMoving();
826 }
827
828 public void undoGravityOverride() {
829 moveHandler.undoGravityOverride();
830 }
831 }
832
833 /** Listener for bubble expansion state change. */
834 public interface BubbleExpansionStateListener {
835 void onBubbleExpansionStateChanged(@ExpansionState int expansionState, boolean isUserAction);
836 }
837}