blob: 3378ad81a8a2a806fe9910ddf3d20791253d95c3 [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;
yueg87111362017-12-08 12:45:50 -080020import android.animation.AnimatorListenerAdapter;
Eric Erfanian938468d2017-10-24 14:05:52 -070021import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
yueg81a77ff2017-12-05 10:29:03 -080023import android.animation.ValueAnimator;
Eric Erfanian938468d2017-10-24 14:05:52 -070024import android.annotation.SuppressLint;
25import android.app.PendingIntent.CanceledException;
26import android.content.Context;
27import android.content.Intent;
28import android.graphics.PixelFormat;
yueg0b4755c2017-12-18 10:01:03 -080029import android.graphics.Rect;
Eric Erfanian938468d2017-10-24 14:05:52 -070030import android.graphics.drawable.Animatable;
31import android.graphics.drawable.Drawable;
Eric Erfanian938468d2017-10-24 14:05:52 -070032import android.net.Uri;
33import android.os.Handler;
34import android.provider.Settings;
Eric Erfanian938468d2017-10-24 14:05:52 -070035import android.support.annotation.IntDef;
36import android.support.annotation.NonNull;
37import android.support.annotation.Nullable;
38import android.support.annotation.VisibleForTesting;
39import android.support.v4.graphics.ColorUtils;
Eric Erfanian938468d2017-10-24 14:05:52 -070040import android.support.v4.os.BuildCompat;
Eric Erfanian938468d2017-10-24 14:05:52 -070041import android.support.v4.view.animation.LinearOutSlowInInterpolator;
42import android.transition.TransitionManager;
43import android.transition.TransitionValues;
44import android.view.ContextThemeWrapper;
45import android.view.Gravity;
46import android.view.LayoutInflater;
47import android.view.MotionEvent;
48import android.view.View;
yuega235e132017-12-13 14:13:57 -080049import android.view.View.AccessibilityDelegate;
Eric Erfanian938468d2017-10-24 14:05:52 -070050import android.view.ViewGroup;
51import android.view.ViewPropertyAnimator;
52import android.view.ViewTreeObserver.OnPreDrawListener;
53import android.view.WindowManager;
54import android.view.WindowManager.LayoutParams;
yuega235e132017-12-13 14:13:57 -080055import android.view.accessibility.AccessibilityNodeInfo;
56import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
yueg0b4755c2017-12-18 10:01:03 -080057import android.view.animation.AccelerateDecelerateInterpolator;
Eric Erfanian938468d2017-10-24 14:05:52 -070058import android.view.animation.AnticipateInterpolator;
59import android.view.animation.OvershootInterpolator;
60import android.widget.ImageView;
61import android.widget.TextView;
62import android.widget.ViewAnimator;
yueg81a77ff2017-12-05 10:29:03 -080063import com.android.dialer.common.LogUtil;
64import com.android.dialer.logging.DialerImpression;
65import com.android.dialer.logging.Logger;
yuega5a08d82017-10-31 14:11:53 -070066import com.android.dialer.util.DrawableConverter;
yueg81a77ff2017-12-05 10:29:03 -080067import com.android.incallui.call.CallList;
68import com.android.incallui.call.DialerCall;
Eric Erfanian938468d2017-10-24 14:05:52 -070069import com.android.newbubble.NewBubbleInfo.Action;
70import java.lang.annotation.Retention;
71import java.lang.annotation.RetentionPolicy;
72import java.util.List;
73
74/**
75 * Creates and manages a bubble window from information in a {@link NewBubbleInfo}. Before creating,
76 * be sure to check whether bubbles may be shown using {@link #canShowBubbles(Context)} and request
77 * permission if necessary ({@link #getRequestPermissionIntent(Context)} is provided for
78 * convenience)
79 */
80public class NewBubble {
81 // This class has some odd behavior that is not immediately obvious in order to avoid jank when
82 // resizing. See http://go/bubble-resize for details.
83
84 // How long text should show after showText(CharSequence) is called
85 private static final int SHOW_TEXT_DURATION_MILLIS = 3000;
86 // How long the new window should show before destroying the old one during resize operations.
87 // This ensures the new window has had time to draw first.
88 private static final int WINDOW_REDRAW_DELAY_MILLIS = 50;
89
yueg0b4755c2017-12-18 10:01:03 -080090 private static final int EXPAND_AND_COLLAPSE_ANIMATION_DURATION = 200;
91
Eric Erfanian938468d2017-10-24 14:05:52 -070092 private static Boolean canShowBubblesForTesting = null;
93
yueg0b4755c2017-12-18 10:01:03 -080094 private final AccelerateDecelerateInterpolator accelerateDecelerateInterpolator =
95 new AccelerateDecelerateInterpolator();
96
Eric Erfanian938468d2017-10-24 14:05:52 -070097 private final Context context;
98 private final WindowManager windowManager;
99
100 private final Handler handler;
101 private LayoutParams windowParams;
102
103 // Initialized in factory method
104 @SuppressWarnings("NullableProblems")
105 @NonNull
106 private NewBubbleInfo currentInfo;
107
108 @Visibility private int visibility;
109 private boolean expanded;
110 private boolean textShowing;
111 private boolean hideAfterText;
112 private CharSequence textAfterShow;
113 private int collapseEndAction;
114
yueg81a77ff2017-12-05 10:29:03 -0800115 ViewHolder viewHolder;
Eric Erfanian938468d2017-10-24 14:05:52 -0700116 private ViewPropertyAnimator collapseAnimation;
117 private Integer overrideGravity;
yueg87111362017-12-08 12:45:50 -0800118 @VisibleForTesting AnimatorSet exitAnimatorSet;
Eric Erfanian938468d2017-10-24 14:05:52 -0700119
yueg87111362017-12-08 12:45:50 -0800120 private final int primaryIconMoveDistance;
121 private final int leftBoundary;
yueg81a77ff2017-12-05 10:29:03 -0800122 private int savedYPosition = -1;
123
Eric Erfanian938468d2017-10-24 14:05:52 -0700124 private final Runnable collapseRunnable =
125 new Runnable() {
126 @Override
127 public void run() {
128 textShowing = false;
129 if (hideAfterText) {
130 // Always reset here since text shouldn't keep showing.
131 hideAndReset();
132 } else {
yueg81a77ff2017-12-05 10:29:03 -0800133 viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_AVATAR_AND_ICON);
Eric Erfanian938468d2017-10-24 14:05:52 -0700134 }
135 }
136 };
137
Eric Erfanian938468d2017-10-24 14:05:52 -0700138 /** Type of action after bubble collapse */
139 @Retention(RetentionPolicy.SOURCE)
140 @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE})
141 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
142 public @interface CollapseEnd {
143 int NOTHING = 0;
144 int HIDE = 1;
145 }
146
147 @Retention(RetentionPolicy.SOURCE)
148 @IntDef({Visibility.ENTERING, Visibility.SHOWING, Visibility.EXITING, Visibility.HIDDEN})
149 private @interface Visibility {
150 int HIDDEN = 0;
151 int ENTERING = 1;
152 int SHOWING = 2;
153 int EXITING = 3;
154 }
155
156 /** Indicate bubble expansion state. */
157 @Retention(RetentionPolicy.SOURCE)
158 @IntDef({ExpansionState.START_EXPANDING, ExpansionState.START_COLLAPSING})
159 public @interface ExpansionState {
160 // TODO(yueg): add more states when needed
161 int START_EXPANDING = 0;
162 int START_COLLAPSING = 1;
163 }
164
165 /**
166 * Determines whether bubbles can be shown based on permissions obtained. This should be checked
167 * before attempting to create a Bubble.
168 *
169 * @return true iff bubbles are able to be shown.
170 * @see Settings#canDrawOverlays(Context)
171 */
172 public static boolean canShowBubbles(@NonNull Context context) {
173 return canShowBubblesForTesting != null
174 ? canShowBubblesForTesting
175 : Settings.canDrawOverlays(context);
176 }
177
178 @VisibleForTesting(otherwise = VisibleForTesting.NONE)
179 public static void setCanShowBubblesForTesting(boolean canShowBubbles) {
180 canShowBubblesForTesting = canShowBubbles;
181 }
182
183 /** Returns an Intent to request permission to show overlays */
184 @NonNull
185 public static Intent getRequestPermissionIntent(@NonNull Context context) {
186 return new Intent(
187 Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
188 Uri.fromParts("package", context.getPackageName(), null));
189 }
190
191 /** Creates instances of Bubble. The default implementation just calls the constructor. */
192 @VisibleForTesting
193 public interface BubbleFactory {
194 NewBubble createBubble(@NonNull Context context, @NonNull Handler handler);
195 }
196
197 private static BubbleFactory bubbleFactory = NewBubble::new;
198
199 public static NewBubble createBubble(@NonNull Context context, @NonNull NewBubbleInfo info) {
200 NewBubble bubble = bubbleFactory.createBubble(context, new Handler());
201 bubble.setBubbleInfo(info);
202 return bubble;
203 }
204
205 @VisibleForTesting
206 public static void setBubbleFactory(@NonNull BubbleFactory bubbleFactory) {
207 NewBubble.bubbleFactory = bubbleFactory;
208 }
209
210 @VisibleForTesting
211 public static void resetBubbleFactory() {
212 NewBubble.bubbleFactory = NewBubble::new;
213 }
214
215 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
216 NewBubble(@NonNull Context context, @NonNull Handler handler) {
217 context = new ContextThemeWrapper(context, R.style.Theme_AppCompat);
218 this.context = context;
219 this.handler = handler;
220 windowManager = context.getSystemService(WindowManager.class);
221
222 viewHolder = new ViewHolder(context);
yueg81a77ff2017-12-05 10:29:03 -0800223
224 leftBoundary =
225 context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal)
226 - context
227 .getResources()
228 .getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal);
yueg87111362017-12-08 12:45:50 -0800229 primaryIconMoveDistance =
230 context.getResources().getDimensionPixelSize(R.dimen.bubble_size)
231 - context.getResources().getDimensionPixelSize(R.dimen.bubble_small_icon_size);
Eric Erfanian938468d2017-10-24 14:05:52 -0700232 }
233
234 /** Expands the main bubble menu. */
yuegf539f782017-12-18 16:20:58 -0800235 public void expand() {
yuega235e132017-12-13 14:13:57 -0800236 setPrimaryButtonAccessibilityAction(
237 context.getString(R.string.a11y_bubble_primary_button_collapse_action));
yueg0b4755c2017-12-18 10:01:03 -0800238
yueg81a77ff2017-12-05 10:29:03 -0800239 viewHolder.setDrawerVisibility(View.INVISIBLE);
yueg0b4755c2017-12-18 10:01:03 -0800240 viewHolder.getArrow().setVisibility(View.INVISIBLE);
241 // No click during animation to avoid jank.
242 viewHolder.setPrimaryButtonClickable(false);
243
Eric Erfanian938468d2017-10-24 14:05:52 -0700244 View expandedView = viewHolder.getExpandedView();
245 expandedView
246 .getViewTreeObserver()
247 .addOnPreDrawListener(
248 new OnPreDrawListener() {
249 @Override
250 public boolean onPreDraw() {
yueg81a77ff2017-12-05 10:29:03 -0800251 // Move the whole bubble up so that expanded view is still in screen
252 int moveUpDistance = viewHolder.getMoveUpDistance();
253 if (moveUpDistance != 0) {
254 savedYPosition = windowParams.y;
255 }
256
yueg0b4755c2017-12-18 10:01:03 -0800257 // Animation 1: animate x-move and y-move (if needed) together
yueg81a77ff2017-12-05 10:29:03 -0800258 int deltaX =
259 (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
yueg0b4755c2017-12-18 10:01:03 -0800260 float k = -(float) moveUpDistance / deltaX;
yueg81a77ff2017-12-05 10:29:03 -0800261 if (isDrawingFromRight()) {
262 deltaX = -deltaX;
263 }
yueg0b4755c2017-12-18 10:01:03 -0800264 ValueAnimator xValueAnimator =
265 createBubbleMoveAnimator(
266 windowParams.x - deltaX, windowParams.x, windowParams.y, k);
yueg81a77ff2017-12-05 10:29:03 -0800267
yueg0b4755c2017-12-18 10:01:03 -0800268 // Show expanded view
269 expandedView.setVisibility(View.VISIBLE);
yueg81a77ff2017-12-05 10:29:03 -0800270
yueg0b4755c2017-12-18 10:01:03 -0800271 // Animator 2: reveal expanded view from top left or top right
272 View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
273 ValueAnimator revealAnim =
274 createOpenCloseOutlineProvider(expandedMenu)
275 .createRevealAnimator(expandedMenu, false);
276 revealAnim.setInterpolator(accelerateDecelerateInterpolator);
277
278 // Animator 3: expanded view fade in
279 Animator fadeIn = ObjectAnimator.ofFloat(expandedView, "alpha", 0, 1);
280 fadeIn.setInterpolator(accelerateDecelerateInterpolator);
281
282 // Play all animation together
283 AnimatorSet expandAnimatorSet = new AnimatorSet();
284 expandAnimatorSet.playTogether(revealAnim, fadeIn, xValueAnimator);
285 expandAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
286 expandAnimatorSet.addListener(
yueg87111362017-12-08 12:45:50 -0800287 new AnimatorListenerAdapter() {
yueg81a77ff2017-12-05 10:29:03 -0800288 @Override
289 public void onAnimationEnd(Animator animation) {
yueg0b4755c2017-12-18 10:01:03 -0800290 // Show arrow after animation
291 viewHolder.getArrow().setVisibility(View.VISIBLE);
292 // Safe to click primary button now
293 viewHolder.setPrimaryButtonClickable(true);
yueg81a77ff2017-12-05 10:29:03 -0800294 }
yueg81a77ff2017-12-05 10:29:03 -0800295 });
yueg0b4755c2017-12-18 10:01:03 -0800296 expandAnimatorSet.start();
yueg81a77ff2017-12-05 10:29:03 -0800297
Eric Erfanian938468d2017-10-24 14:05:52 -0700298 expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
Eric Erfanian938468d2017-10-24 14:05:52 -0700299 return false;
300 }
301 });
302 setFocused(true);
303 expanded = true;
304 }
305
yueg81a77ff2017-12-05 10:29:03 -0800306 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
yuegf539f782017-12-18 16:20:58 -0800307 public void startCollapse(@CollapseEnd int endAction, boolean shouldRecoverYPosition) {
yueg81a77ff2017-12-05 10:29:03 -0800308 View expandedView = viewHolder.getExpandedView();
309 if (expandedView.getVisibility() != View.VISIBLE || collapseAnimation != null) {
310 // Drawer is already collapsed or animation is running.
311 return;
312 }
313
314 overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT;
315 setFocused(false);
316
317 if (collapseEndAction == CollapseEnd.NOTHING) {
318 collapseEndAction = endAction;
319 }
yuega235e132017-12-13 14:13:57 -0800320 setPrimaryButtonAccessibilityAction(
321 context.getString(R.string.a11y_bubble_primary_button_expand_action));
yueg0b4755c2017-12-18 10:01:03 -0800322
323 // Hide arrow before animation
324 viewHolder.getArrow().setVisibility(View.INVISIBLE);
325
326 // No click during animation to avoid jank.
327 viewHolder.setPrimaryButtonClickable(false);
328
329 // Calculate animation values
330 int deltaX = (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
331 float k =
332 (savedYPosition != -1 && shouldRecoverYPosition)
333 ? (savedYPosition - windowParams.y) / (float) deltaX
334 : 0;
335 // The position is not useful after collapse
336 savedYPosition = -1;
337
338 // Animation 1: animate x-move and y-move (if needed) together
339 ValueAnimator xValueAnimator =
340 createBubbleMoveAnimator(windowParams.x, windowParams.x - deltaX, windowParams.y, k);
341
342 // Animator 2: hide expanded view to top left or top right
343 View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
344 ValueAnimator revealAnim =
345 createOpenCloseOutlineProvider(expandedMenu).createRevealAnimator(expandedMenu, true);
346 revealAnim.setInterpolator(accelerateDecelerateInterpolator);
347
348 // Animator 3: expanded view fade out
349 Animator fadeOut = ObjectAnimator.ofFloat(expandedView, "alpha", 1, 0);
350 fadeOut.setInterpolator(accelerateDecelerateInterpolator);
351
352 // Play all animation together
353 AnimatorSet collapseAnimatorSet = new AnimatorSet();
354 collapseAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
355 collapseAnimatorSet.playTogether(revealAnim, fadeOut, xValueAnimator);
356 collapseAnimatorSet.addListener(
357 new AnimatorListenerAdapter() {
358 @Override
359 public void onAnimationEnd(Animator animation) {
360 collapseAnimation = null;
361 expanded = false;
362
363 if (textShowing) {
364 // Will do resize once the text is done.
365 return;
366 }
367
368 // If this collapse was to come before a hide, do it now.
369 if (collapseEndAction == CollapseEnd.HIDE) {
370 hide();
371 }
372 collapseEndAction = CollapseEnd.NOTHING;
373
374 // If collapse on the right side, the primary button move left a bit after drawer
375 // visibility becoming GONE. To avoid it, we create a new ViewHolder.
376 // It also set primary button clickable back to true, so no need to reset manually.
377 replaceViewHolder();
378
379 // Resume normal gravity after any resizing is done.
380 handler.postDelayed(
yueg81a77ff2017-12-05 10:29:03 -0800381 () -> {
yueg0b4755c2017-12-18 10:01:03 -0800382 overrideGravity = null;
383 if (!viewHolder.isMoving()) {
384 viewHolder.undoGravityOverride();
yueg81a77ff2017-12-05 10:29:03 -0800385 }
yueg0b4755c2017-12-18 10:01:03 -0800386 },
387 // Need to wait twice as long for resize and layout
388 WINDOW_REDRAW_DELAY_MILLIS * 2);
389 }
390 });
391 collapseAnimatorSet.start();
yueg81a77ff2017-12-05 10:29:03 -0800392 }
393
Eric Erfanian938468d2017-10-24 14:05:52 -0700394 /**
395 * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is
396 * already showing this method does nothing.
397 */
398 public void show() {
399 if (collapseEndAction == CollapseEnd.HIDE) {
400 // If show() was called while collapsing, make sure we don't hide after.
401 collapseEndAction = CollapseEnd.NOTHING;
402 }
403 if (visibility == Visibility.SHOWING || visibility == Visibility.ENTERING) {
404 return;
405 }
406
407 hideAfterText = false;
408
409 if (windowParams == null) {
410 // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O.
411 @SuppressWarnings("deprecation")
412 @SuppressLint("InlinedApi")
413 int type =
414 BuildCompat.isAtLeastO()
415 ? LayoutParams.TYPE_APPLICATION_OVERLAY
416 : LayoutParams.TYPE_PHONE;
417
418 windowParams =
419 new LayoutParams(
420 type,
421 LayoutParams.FLAG_NOT_TOUCH_MODAL
422 | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
423 | LayoutParams.FLAG_NOT_FOCUSABLE
424 | LayoutParams.FLAG_LAYOUT_NO_LIMITS,
425 PixelFormat.TRANSLUCENT);
426 windowParams.gravity = Gravity.TOP | Gravity.LEFT;
yueg81a77ff2017-12-05 10:29:03 -0800427 windowParams.x = leftBoundary;
Eric Erfanian938468d2017-10-24 14:05:52 -0700428 windowParams.y = currentInfo.getStartingYPosition();
429 windowParams.height = LayoutParams.WRAP_CONTENT;
430 windowParams.width = LayoutParams.WRAP_CONTENT;
431 }
432
yueg87111362017-12-08 12:45:50 -0800433 if (exitAnimatorSet != null) {
434 exitAnimatorSet.removeAllListeners();
435 exitAnimatorSet.cancel();
436 exitAnimatorSet = null;
Eric Erfanian938468d2017-10-24 14:05:52 -0700437 } else {
438 windowManager.addView(viewHolder.getRoot(), windowParams);
yueg87111362017-12-08 12:45:50 -0800439 viewHolder.getPrimaryButton().setVisibility(View.VISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700440 viewHolder.getPrimaryButton().setScaleX(0);
441 viewHolder.getPrimaryButton().setScaleY(0);
yueg87111362017-12-08 12:45:50 -0800442 viewHolder.getPrimaryAvatar().setAlpha(0f);
443 viewHolder.getPrimaryIcon().setAlpha(0f);
Eric Erfanian938468d2017-10-24 14:05:52 -0700444 }
445
446 viewHolder.setChildClickable(true);
447 visibility = Visibility.ENTERING;
yueg87111362017-12-08 12:45:50 -0800448
yuega235e132017-12-13 14:13:57 -0800449 setPrimaryButtonAccessibilityAction(
450 context.getString(R.string.a11y_bubble_primary_button_expand_action));
451
yueg87111362017-12-08 12:45:50 -0800452 // Show bubble animation: scale the whole bubble to 1, and change avatar+icon's alpha to 1
453 ObjectAnimator scaleXAnimator =
454 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 1);
455 ObjectAnimator scaleYAnimator =
456 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 1);
457 ObjectAnimator avatarAlphaAnimator =
458 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 1);
459 ObjectAnimator iconAlphaAnimator =
460 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 1);
461 AnimatorSet enterAnimatorSet = new AnimatorSet();
462 enterAnimatorSet.playTogether(
463 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
464 enterAnimatorSet.setInterpolator(new OvershootInterpolator());
465 enterAnimatorSet.addListener(
466 new AnimatorListenerAdapter() {
467 @Override
468 public void onAnimationEnd(Animator animation) {
469 visibility = Visibility.SHOWING;
470 // Show the queued up text, if available.
471 if (textAfterShow != null) {
472 showText(textAfterShow);
473 textAfterShow = null;
474 }
475 }
476 });
477 enterAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700478
479 updatePrimaryIconAnimation();
480 }
481
482 /** Hide the bubble. */
483 public void hide() {
484 if (hideAfterText) {
485 // hideAndReset() will be called after showing text, do nothing here.
486 return;
487 }
488 hideHelper(this::defaultAfterHidingAnimation);
489 }
490
491 /** Hide the bubble and reset {@viewHolder} to initial state */
492 public void hideAndReset() {
493 hideHelper(
494 () -> {
495 defaultAfterHidingAnimation();
496 reset();
497 });
498 }
499
500 /** Returns whether the bubble is currently visible */
501 public boolean isVisible() {
502 return visibility == Visibility.SHOWING
503 || visibility == Visibility.ENTERING
504 || visibility == Visibility.EXITING;
505 }
506
507 /**
508 * Set the info for this Bubble to display
509 *
510 * @param bubbleInfo the BubbleInfo to display in this Bubble.
511 */
512 public void setBubbleInfo(@NonNull NewBubbleInfo bubbleInfo) {
513 currentInfo = bubbleInfo;
514 update();
515 }
516
517 /**
518 * Update the state and behavior of actions.
519 *
520 * @param actions the new state of the bubble's actions
521 */
522 public void updateActions(@NonNull List<Action> actions) {
523 currentInfo = NewBubbleInfo.from(currentInfo).setActions(actions).build();
524 updateButtonStates();
525 }
526
yuega5a08d82017-10-31 14:11:53 -0700527 /**
528 * Update the avatar from photo.
529 *
530 * @param avatar the new photo avatar in the bubble's primary button
531 */
532 public void updatePhotoAvatar(@NonNull Drawable avatar) {
533 // Make it round
534 int bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
535 Drawable roundAvatar =
536 DrawableConverter.getRoundedDrawable(context, avatar, bubbleSize, bubbleSize);
537
538 updateAvatar(roundAvatar);
539 }
540
541 /**
542 * Update the avatar.
543 *
544 * @param avatar the new avatar in the bubble's primary button
545 */
546 public void updateAvatar(@NonNull Drawable avatar) {
547 if (!avatar.equals(currentInfo.getAvatar())) {
548 currentInfo = NewBubbleInfo.from(currentInfo).setAvatar(avatar).build();
549 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
550 }
551 }
552
Eric Erfanian938468d2017-10-24 14:05:52 -0700553 /** Returns the currently displayed NewBubbleInfo */
554 public NewBubbleInfo getBubbleInfo() {
555 return currentInfo;
556 }
557
558 /**
559 * Display text in the main bubble. The bubble's drawer is not expandable while text is showing,
560 * and the drawer will be closed if already open.
561 *
562 * @param text the text to display to the user
563 */
564 public void showText(@NonNull CharSequence text) {
565 textShowing = true;
566 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800567 startCollapse(CollapseEnd.NOTHING, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700568 doShowText(text);
569 } else {
570 // Need to transition from old bounds to new bounds manually
571 NewChangeOnScreenBounds transition = new NewChangeOnScreenBounds();
572 // Prepare and capture start values
573 TransitionValues startValues = new TransitionValues();
574 startValues.view = viewHolder.getPrimaryButton();
575 transition.addTarget(startValues.view);
576 transition.captureStartValues(startValues);
577
578 // If our view is not laid out yet, postpone showing the text.
579 if (startValues.values.isEmpty()) {
580 textAfterShow = text;
581 return;
582 }
583
yueg81a77ff2017-12-05 10:29:03 -0800584 doShowText(text);
585 // Hide the text so we can animate it in
586 viewHolder.getPrimaryText().setAlpha(0);
Eric Erfanian938468d2017-10-24 14:05:52 -0700587
yueg81a77ff2017-12-05 10:29:03 -0800588 ViewAnimator primaryButton = viewHolder.getPrimaryButton();
589 // Cancel the automatic transition scheduled in doShowText
590 TransitionManager.endTransitions((ViewGroup) primaryButton.getParent());
591 primaryButton
592 .getViewTreeObserver()
593 .addOnPreDrawListener(
594 new OnPreDrawListener() {
595 @Override
596 public boolean onPreDraw() {
597 primaryButton.getViewTreeObserver().removeOnPreDrawListener(this);
Eric Erfanian938468d2017-10-24 14:05:52 -0700598
yueg81a77ff2017-12-05 10:29:03 -0800599 // Prepare and capture end values, always use the size of primaryText since
600 // its invisibility makes primaryButton smaller than expected
601 TransitionValues endValues = new TransitionValues();
602 endValues.values.put(
603 NewChangeOnScreenBounds.PROPNAME_WIDTH,
604 viewHolder.getPrimaryText().getWidth());
605 endValues.values.put(
606 NewChangeOnScreenBounds.PROPNAME_HEIGHT,
607 viewHolder.getPrimaryText().getHeight());
608 endValues.view = primaryButton;
609 transition.addTarget(endValues.view);
610 transition.captureEndValues(endValues);
Eric Erfanian938468d2017-10-24 14:05:52 -0700611
yueg81a77ff2017-12-05 10:29:03 -0800612 // animate the primary button bounds change
613 Animator bounds =
614 transition.createAnimator(primaryButton, startValues, endValues);
Eric Erfanian938468d2017-10-24 14:05:52 -0700615
yueg81a77ff2017-12-05 10:29:03 -0800616 // Animate the text in
617 Animator alpha =
618 ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f);
Eric Erfanian938468d2017-10-24 14:05:52 -0700619
yueg81a77ff2017-12-05 10:29:03 -0800620 AnimatorSet set = new AnimatorSet();
621 set.play(bounds).before(alpha);
622 set.start();
623 return false;
624 }
625 });
Eric Erfanian938468d2017-10-24 14:05:52 -0700626 }
627 handler.removeCallbacks(collapseRunnable);
628 handler.postDelayed(collapseRunnable, SHOW_TEXT_DURATION_MILLIS);
629 }
630
Eric Erfanian938468d2017-10-24 14:05:52 -0700631 @Nullable
632 Integer getGravityOverride() {
633 return overrideGravity;
634 }
635
636 void onMoveStart() {
yueg81a77ff2017-12-05 10:29:03 -0800637 if (viewHolder.getExpandedView().getVisibility() == View.VISIBLE) {
638 viewHolder.setDrawerVisibility(View.INVISIBLE);
639 }
640 expanded = false;
641 savedYPosition = -1;
642
Eric Erfanian938468d2017-10-24 14:05:52 -0700643 viewHolder
644 .getPrimaryButton()
645 .animate()
646 .translationZ(
yuegc6deafc2017-11-06 16:42:13 -0800647 context
648 .getResources()
649 .getDimensionPixelOffset(R.dimen.bubble_dragging_elevation_change));
Eric Erfanian938468d2017-10-24 14:05:52 -0700650 }
651
652 void onMoveFinish() {
653 viewHolder.getPrimaryButton().animate().translationZ(0);
Eric Erfanian938468d2017-10-24 14:05:52 -0700654 }
655
656 void primaryButtonClick() {
657 if (textShowing || currentInfo.getActions().isEmpty()) {
658 return;
659 }
660 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800661 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
662 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700663 } else {
yuegf539f782017-12-18 16:20:58 -0800664 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_EXPAND);
665 expand();
Eric Erfanian938468d2017-10-24 14:05:52 -0700666 }
667 }
668
yueg81a77ff2017-12-05 10:29:03 -0800669 void onLeftRightSwitch(boolean onRight) {
yueg87111362017-12-08 12:45:50 -0800670 // Move primary icon to the other side so it's not partially hiden
yueg81a77ff2017-12-05 10:29:03 -0800671 View primaryIcon = viewHolder.getPrimaryIcon();
yueg87111362017-12-08 12:45:50 -0800672 primaryIcon.animate().translationX(onRight ? -primaryIconMoveDistance : 0).start();
yueg81a77ff2017-12-05 10:29:03 -0800673 }
674
Eric Erfanian938468d2017-10-24 14:05:52 -0700675 LayoutParams getWindowParams() {
676 return windowParams;
677 }
678
679 View getRootView() {
680 return viewHolder.getRoot();
681 }
682
683 /**
684 * Hide the bubble if visible. Will run a short exit animation and before hiding, and {@code
685 * afterHiding} after hiding. If the bubble is currently showing text, will hide after the text is
686 * done displaying. If the bubble is not visible this method does nothing.
687 */
yueg87111362017-12-08 12:45:50 -0800688 @VisibleForTesting
689 void hideHelper(Runnable afterHiding) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700690 if (visibility == Visibility.HIDDEN || visibility == Visibility.EXITING) {
691 return;
692 }
693
694 // Make bubble non clickable to prevent further buggy actions
695 viewHolder.setChildClickable(false);
696
697 if (textShowing) {
698 hideAfterText = true;
699 return;
700 }
701
702 if (collapseAnimation != null) {
703 collapseEndAction = CollapseEnd.HIDE;
704 return;
705 }
706
707 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800708 startCollapse(CollapseEnd.HIDE, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700709 return;
710 }
711
712 visibility = Visibility.EXITING;
yueg87111362017-12-08 12:45:50 -0800713
714 // Hide bubble animation: scale the whole bubble to 0, and change avatar+icon's alpha to 0
715 ObjectAnimator scaleXAnimator =
716 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 0);
717 ObjectAnimator scaleYAnimator =
718 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 0);
719 ObjectAnimator avatarAlphaAnimator =
720 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 0);
721 ObjectAnimator iconAlphaAnimator =
722 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 0);
723 exitAnimatorSet = new AnimatorSet();
724 exitAnimatorSet.playTogether(
725 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
726 exitAnimatorSet.setInterpolator(new AnticipateInterpolator());
727 exitAnimatorSet.addListener(
728 new AnimatorListenerAdapter() {
729 @Override
yuega235e132017-12-13 14:13:57 -0800730 public void onAnimationStart(Animator animation) {
731 viewHolder.getPrimaryButton().setAccessibilityDelegate(null);
732 }
733
734 @Override
yueg87111362017-12-08 12:45:50 -0800735 public void onAnimationEnd(Animator animation) {
736 afterHiding.run();
737 }
738 });
739 exitAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700740 }
741
742 private void reset() {
743 viewHolder = new ViewHolder(viewHolder.getRoot().getContext());
744 update();
745 }
746
747 private void update() {
yuega5a08d82017-10-31 14:11:53 -0700748 // Whole primary button background
yueg84ac49b2017-11-01 16:22:28 -0700749 Drawable backgroundCirle =
750 context.getResources().getDrawable(R.drawable.bubble_shape_circle, context.getTheme());
Eric Erfanian938468d2017-10-24 14:05:52 -0700751 int primaryTint =
752 ColorUtils.compositeColors(
753 context.getColor(R.color.bubble_primary_background_darken),
754 currentInfo.getPrimaryColor());
yueg84ac49b2017-11-01 16:22:28 -0700755 backgroundCirle.mutate().setTint(primaryTint);
756 viewHolder.getPrimaryButton().setBackground(backgroundCirle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700757
yuega5a08d82017-10-31 14:11:53 -0700758 // Small icon
yueg84ac49b2017-11-01 16:22:28 -0700759 Drawable smallIconBackgroundCircle =
760 context
761 .getResources()
762 .getDrawable(R.drawable.bubble_shape_circle_small, context.getTheme());
763 smallIconBackgroundCircle.setTint(context.getColor(R.color.bubble_button_color_blue));
764 viewHolder.getPrimaryIcon().setBackground(smallIconBackgroundCircle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700765 viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon());
yuega5a08d82017-10-31 14:11:53 -0700766 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
Eric Erfanian938468d2017-10-24 14:05:52 -0700767
yuega5a08d82017-10-31 14:11:53 -0700768 updatePrimaryIconAnimation();
Eric Erfanian938468d2017-10-24 14:05:52 -0700769 updateButtonStates();
770 }
771
772 private void updatePrimaryIconAnimation() {
773 Drawable drawable = viewHolder.getPrimaryIcon().getDrawable();
774 if (drawable instanceof Animatable) {
775 if (isVisible()) {
776 ((Animatable) drawable).start();
777 } else {
778 ((Animatable) drawable).stop();
779 }
780 }
781 }
782
783 private void updateButtonStates() {
yueg84ac49b2017-11-01 16:22:28 -0700784 configureButton(currentInfo.getActions().get(0), viewHolder.getFullScreenButton());
785 configureButton(currentInfo.getActions().get(1), viewHolder.getMuteButton());
786 configureButton(currentInfo.getActions().get(2), viewHolder.getAudioRouteButton());
787 configureButton(currentInfo.getActions().get(3), viewHolder.getEndCallButton());
Eric Erfanian938468d2017-10-24 14:05:52 -0700788 }
789
yueg87111362017-12-08 12:45:50 -0800790 @VisibleForTesting
791 void doShowText(@NonNull CharSequence text) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700792 TransitionManager.beginDelayedTransition((ViewGroup) viewHolder.getPrimaryButton().getParent());
793 viewHolder.getPrimaryText().setText(text);
794 viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_TEXT);
795 }
796
yueg84ac49b2017-11-01 16:22:28 -0700797 private void configureButton(Action action, NewCheckableButton button) {
798 button.setCompoundDrawablesWithIntrinsicBounds(action.getIconDrawable(), null, null, null);
Eric Erfanian938468d2017-10-24 14:05:52 -0700799 button.setChecked(action.isChecked());
800 button.setEnabled(action.isEnabled());
yueg84ac49b2017-11-01 16:22:28 -0700801 button.setText(action.getName());
yuega235e132017-12-13 14:13:57 -0800802 button.setContentDescription(action.getName());
Eric Erfanian938468d2017-10-24 14:05:52 -0700803 button.setOnClickListener(v -> doAction(action));
804 }
805
806 private void doAction(Action action) {
807 try {
808 action.getIntent().send();
809 } catch (CanceledException e) {
810 throw new RuntimeException(e);
811 }
812 }
813
yueg81a77ff2017-12-05 10:29:03 -0800814 /**
815 * Create a new ViewHolder object to replace the old one.It only happens when not moving and
816 * collapsed.
817 */
818 void replaceViewHolder() {
819 LogUtil.enterBlock("NewBubble.replaceViewHolder");
Eric Erfanian938468d2017-10-24 14:05:52 -0700820 ViewHolder oldViewHolder = viewHolder;
Eric Erfanian938468d2017-10-24 14:05:52 -0700821
yueg81a77ff2017-12-05 10:29:03 -0800822 // Create a new ViewHolder and copy needed info.
823 viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext());
824 viewHolder
825 .getPrimaryButton()
826 .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild());
827 viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText());
yueg87111362017-12-08 12:45:50 -0800828 viewHolder.getPrimaryIcon().setX(isDrawingFromRight() ? 0 : primaryIconMoveDistance);
yueg81a77ff2017-12-05 10:29:03 -0800829 viewHolder
830 .getPrimaryIcon()
yueg87111362017-12-08 12:45:50 -0800831 .setTranslationX(isDrawingFromRight() ? -primaryIconMoveDistance : 0);
yuega235e132017-12-13 14:13:57 -0800832 setPrimaryButtonAccessibilityAction(
833 context.getString(R.string.a11y_bubble_primary_button_expand_action));
Eric Erfanian938468d2017-10-24 14:05:52 -0700834
yueg81a77ff2017-12-05 10:29:03 -0800835 update();
836
837 // Add new view at its horizontal boundary
Eric Erfanian938468d2017-10-24 14:05:52 -0700838 ViewGroup root = viewHolder.getRoot();
yueg81a77ff2017-12-05 10:29:03 -0800839 windowParams.x = leftBoundary;
840 windowParams.gravity = Gravity.TOP | (isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT);
Eric Erfanian938468d2017-10-24 14:05:52 -0700841 windowManager.addView(root, windowParams);
yueg81a77ff2017-12-05 10:29:03 -0800842
843 // Remove the old view after delay
Eric Erfanian938468d2017-10-24 14:05:52 -0700844 root.getViewTreeObserver()
845 .addOnPreDrawListener(
846 new OnPreDrawListener() {
847 @Override
848 public boolean onPreDraw() {
849 root.getViewTreeObserver().removeOnPreDrawListener(this);
850 // Wait a bit before removing the old view; make sure the new one has drawn over it.
851 handler.postDelayed(
852 () -> windowManager.removeView(oldViewHolder.getRoot()),
853 WINDOW_REDRAW_DELAY_MILLIS);
854 return true;
855 }
856 });
857 }
858
yueg81a77ff2017-12-05 10:29:03 -0800859 int getDrawerVisibility() {
860 return viewHolder.getExpandedView().getVisibility();
Eric Erfanian938468d2017-10-24 14:05:52 -0700861 }
862
863 private boolean isDrawingFromRight() {
864 return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
865 }
866
867 private void setFocused(boolean focused) {
868 if (focused) {
869 windowParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
870 } else {
871 windowParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
872 }
873 windowManager.updateViewLayout(getRootView(), windowParams);
874 }
875
876 private void defaultAfterHidingAnimation() {
yueg87111362017-12-08 12:45:50 -0800877 exitAnimatorSet = null;
878 viewHolder.getPrimaryButton().setVisibility(View.INVISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700879 windowManager.removeView(viewHolder.getRoot());
880 visibility = Visibility.HIDDEN;
881
882 updatePrimaryIconAnimation();
883 }
884
yueg81a77ff2017-12-05 10:29:03 -0800885 private void logBasicOrCallImpression(DialerImpression.Type impressionType) {
yuegf539f782017-12-18 16:20:58 -0800886 // Bubble is shown for outgoing, active or background call
887 DialerCall call = CallList.getInstance().getOutgoingCall();
888 if (call == null) {
889 call = CallList.getInstance().getActiveOrBackgroundCall();
890 }
yueg81a77ff2017-12-05 10:29:03 -0800891 if (call != null) {
892 Logger.get(context)
893 .logCallImpression(impressionType, call.getUniqueCallId(), call.getTimeAddedMs());
894 } else {
895 Logger.get(context).logImpression(impressionType);
896 }
897 }
898
yuega235e132017-12-13 14:13:57 -0800899 private void setPrimaryButtonAccessibilityAction(String description) {
900 viewHolder
901 .getPrimaryButton()
902 .setAccessibilityDelegate(
903 new AccessibilityDelegate() {
904 @Override
905 public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) {
906 super.onInitializeAccessibilityNodeInfo(v, info);
907
908 AccessibilityAction clickAction =
909 new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, description);
910 info.addAction(clickAction);
911 }
912 });
913 }
914
yueg0b4755c2017-12-18 10:01:03 -0800915 private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider(View view) {
916 int startRectX = isDrawingFromRight() ? view.getMeasuredWidth() : 0;
917 Rect startRect = new Rect(startRectX, 0, startRectX, 0);
918 Rect endRect = new Rect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
919
920 float bubbleRadius = context.getResources().getDimension(R.dimen.bubble_radius);
921 return new RoundedRectRevealOutlineProvider(bubbleRadius, bubbleRadius, startRect, endRect);
922 }
923
924 private ValueAnimator createBubbleMoveAnimator(int startX, int endX, int startY, float k) {
925 ValueAnimator xValueAnimator = ValueAnimator.ofFloat(startX, endX);
926 xValueAnimator.setInterpolator(new LinearOutSlowInInterpolator());
927 xValueAnimator.addUpdateListener(
928 (valueAnimator) -> {
929 // Update windowParams and the root layout.
930 // We can't do ViewPropertyAnimation since it clips children.
931 float newX = (float) valueAnimator.getAnimatedValue();
932 if (k != 0) {
933 windowParams.y = startY + (int) (Math.abs(newX - (float) startX) * k);
934 }
935 windowParams.x = (int) newX;
936 windowManager.updateViewLayout(viewHolder.getRoot(), windowParams);
937 });
938 return xValueAnimator;
939 }
940
Eric Erfanian938468d2017-10-24 14:05:52 -0700941 @VisibleForTesting
942 class ViewHolder {
943
yuega5a08d82017-10-31 14:11:53 -0700944 public static final int CHILD_INDEX_AVATAR_AND_ICON = 0;
Eric Erfanian938468d2017-10-24 14:05:52 -0700945 public static final int CHILD_INDEX_TEXT = 1;
946
yueg0b4755c2017-12-18 10:01:03 -0800947 private NewMoveHandler moveHandler;
Eric Erfanian938468d2017-10-24 14:05:52 -0700948 private final NewWindowRoot root;
949 private final ViewAnimator primaryButton;
950 private final ImageView primaryIcon;
yuega5a08d82017-10-31 14:11:53 -0700951 private final ImageView primaryAvatar;
Eric Erfanian938468d2017-10-24 14:05:52 -0700952 private final TextView primaryText;
yueg0b4755c2017-12-18 10:01:03 -0800953 private final View arrow;
Eric Erfanian938468d2017-10-24 14:05:52 -0700954
955 private final NewCheckableButton fullScreenButton;
956 private final NewCheckableButton muteButton;
957 private final NewCheckableButton audioRouteButton;
958 private final NewCheckableButton endCallButton;
959 private final View expandedView;
960
961 public ViewHolder(Context context) {
962 // Window root is not in the layout file so that the inflater has a view to inflate into
963 this.root = new NewWindowRoot(context);
964 LayoutInflater inflater = LayoutInflater.from(root.getContext());
965 View contentView = inflater.inflate(R.layout.new_bubble_base, root, true);
966 expandedView = contentView.findViewById(R.id.bubble_expanded_layout);
967 primaryButton = contentView.findViewById(R.id.bubble_button_primary);
yuega5a08d82017-10-31 14:11:53 -0700968 primaryAvatar = contentView.findViewById(R.id.bubble_icon_avatar);
Eric Erfanian938468d2017-10-24 14:05:52 -0700969 primaryIcon = contentView.findViewById(R.id.bubble_icon_primary);
970 primaryText = contentView.findViewById(R.id.bubble_text);
yueg0b4755c2017-12-18 10:01:03 -0800971 arrow = contentView.findViewById(R.id.bubble_triangle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700972
973 fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen);
974 muteButton = contentView.findViewById(R.id.bubble_button_mute);
975 audioRouteButton = contentView.findViewById(R.id.bubble_button_audio_route);
976 endCallButton = contentView.findViewById(R.id.bubble_button_end_call);
977
978 root.setOnBackPressedListener(
979 () -> {
980 if (visibility == Visibility.SHOWING && expanded) {
yuegf539f782017-12-18 16:20:58 -0800981 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
982 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700983 return true;
984 }
985 return false;
986 });
987 root.setOnConfigurationChangedListener(
988 (configuration) -> {
989 // The values in the current MoveHandler may be stale, so replace it. Then ensure the
990 // Window is in bounds
991 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
992 moveHandler.snapToBounds();
993 });
994 root.setOnTouchListener(
995 (v, event) -> {
996 if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
yuegf539f782017-12-18 16:20:58 -0800997 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
998 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700999 return true;
1000 }
1001 return false;
1002 });
1003 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
1004 }
1005
1006 private void setChildClickable(boolean clickable) {
1007 fullScreenButton.setClickable(clickable);
1008 muteButton.setClickable(clickable);
1009 audioRouteButton.setClickable(clickable);
1010 endCallButton.setClickable(clickable);
yueg0b4755c2017-12-18 10:01:03 -08001011 setPrimaryButtonClickable(clickable);
1012 }
Eric Erfanian938468d2017-10-24 14:05:52 -07001013
yueg0b4755c2017-12-18 10:01:03 -08001014 private void setPrimaryButtonClickable(boolean clickable) {
Eric Erfanian938468d2017-10-24 14:05:52 -07001015 moveHandler.setClickable(clickable);
1016 }
1017
yueg81a77ff2017-12-05 10:29:03 -08001018 public int getMoveUpDistance() {
1019 int deltaAllowed =
1020 expandedView.getHeight()
1021 - context
1022 .getResources()
1023 .getDimensionPixelOffset(R.dimen.bubble_button_padding_vertical)
1024 * 2;
1025 return moveHandler.getMoveUpDistance(deltaAllowed);
1026 }
1027
Eric Erfanian938468d2017-10-24 14:05:52 -07001028 public ViewGroup getRoot() {
1029 return root;
1030 }
1031
1032 public ViewAnimator getPrimaryButton() {
1033 return primaryButton;
1034 }
1035
1036 public ImageView getPrimaryIcon() {
1037 return primaryIcon;
1038 }
1039
yuega5a08d82017-10-31 14:11:53 -07001040 public ImageView getPrimaryAvatar() {
1041 return primaryAvatar;
1042 }
1043
Eric Erfanian938468d2017-10-24 14:05:52 -07001044 public TextView getPrimaryText() {
1045 return primaryText;
1046 }
1047
1048 public View getExpandedView() {
1049 return expandedView;
1050 }
1051
yueg0b4755c2017-12-18 10:01:03 -08001052 public View getArrow() {
1053 return arrow;
1054 }
1055
Eric Erfanian938468d2017-10-24 14:05:52 -07001056 public NewCheckableButton getFullScreenButton() {
1057 return fullScreenButton;
1058 }
1059
1060 public NewCheckableButton getMuteButton() {
1061 return muteButton;
1062 }
1063
1064 public NewCheckableButton getAudioRouteButton() {
1065 return audioRouteButton;
1066 }
1067
1068 public NewCheckableButton getEndCallButton() {
1069 return endCallButton;
1070 }
1071
1072 public void setDrawerVisibility(int visibility) {
1073 expandedView.setVisibility(visibility);
1074 }
1075
1076 public boolean isMoving() {
1077 return moveHandler.isMoving();
1078 }
1079
1080 public void undoGravityOverride() {
1081 moveHandler.undoGravityOverride();
1082 }
1083 }
Eric Erfanian938468d2017-10-24 14:05:52 -07001084}