blob: 202cfcd336b2b16c28ad799c51ce19c04834335b [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;
yueg07e323c2017-12-19 16:05:47 -080042import android.text.TextUtils;
Eric Erfanian938468d2017-10-24 14:05:52 -070043import android.view.ContextThemeWrapper;
44import android.view.Gravity;
45import android.view.LayoutInflater;
46import android.view.MotionEvent;
47import android.view.View;
yuega235e132017-12-13 14:13:57 -080048import android.view.View.AccessibilityDelegate;
Eric Erfanian938468d2017-10-24 14:05:52 -070049import android.view.ViewGroup;
Eric Erfanian938468d2017-10-24 14:05:52 -070050import android.view.ViewTreeObserver.OnPreDrawListener;
51import android.view.WindowManager;
52import android.view.WindowManager.LayoutParams;
yuega235e132017-12-13 14:13:57 -080053import android.view.accessibility.AccessibilityNodeInfo;
54import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
yueg0b4755c2017-12-18 10:01:03 -080055import android.view.animation.AccelerateDecelerateInterpolator;
Eric Erfanian938468d2017-10-24 14:05:52 -070056import android.view.animation.AnticipateInterpolator;
57import android.view.animation.OvershootInterpolator;
58import android.widget.ImageView;
yuegf473e1d2018-01-02 16:23:14 -080059import android.widget.Toast;
Eric Erfanian938468d2017-10-24 14:05:52 -070060import android.widget.ViewAnimator;
yueg81a77ff2017-12-05 10:29:03 -080061import com.android.dialer.common.LogUtil;
62import com.android.dialer.logging.DialerImpression;
63import com.android.dialer.logging.Logger;
yuega5a08d82017-10-31 14:11:53 -070064import com.android.dialer.util.DrawableConverter;
yueg81a77ff2017-12-05 10:29:03 -080065import com.android.incallui.call.CallList;
66import com.android.incallui.call.DialerCall;
Eric Erfanian938468d2017-10-24 14:05:52 -070067import com.android.newbubble.NewBubbleInfo.Action;
68import java.lang.annotation.Retention;
69import java.lang.annotation.RetentionPolicy;
70import java.util.List;
yueg07e323c2017-12-19 16:05:47 -080071import java.util.Locale;
Eric Erfanian938468d2017-10-24 14:05:52 -070072
73/**
74 * Creates and manages a bubble window from information in a {@link NewBubbleInfo}. Before creating,
75 * be sure to check whether bubbles may be shown using {@link #canShowBubbles(Context)} and request
76 * permission if necessary ({@link #getRequestPermissionIntent(Context)} is provided for
77 * convenience)
78 */
79public class NewBubble {
80 // This class has some odd behavior that is not immediately obvious in order to avoid jank when
81 // resizing. See http://go/bubble-resize for details.
82
Eric Erfanian938468d2017-10-24 14:05:52 -070083 // How long the new window should show before destroying the old one during resize operations.
84 // This ensures the new window has had time to draw first.
85 private static final int WINDOW_REDRAW_DELAY_MILLIS = 50;
86
yueg0b4755c2017-12-18 10:01:03 -080087 private static final int EXPAND_AND_COLLAPSE_ANIMATION_DURATION = 200;
yueg6518fdb2018-01-09 17:30:36 -080088 private static final int HIDE_BUBBLE_ANIMATION_DURATION = 250;
yueg0b4755c2017-12-18 10:01:03 -080089
Eric Erfanian938468d2017-10-24 14:05:52 -070090 private static Boolean canShowBubblesForTesting = null;
91
yueg0b4755c2017-12-18 10:01:03 -080092 private final AccelerateDecelerateInterpolator accelerateDecelerateInterpolator =
93 new AccelerateDecelerateInterpolator();
94
Eric Erfanian938468d2017-10-24 14:05:52 -070095 private final Context context;
96 private final WindowManager windowManager;
97
98 private final Handler handler;
99 private LayoutParams windowParams;
100
101 // Initialized in factory method
102 @SuppressWarnings("NullableProblems")
103 @NonNull
104 private NewBubbleInfo currentInfo;
105
106 @Visibility private int visibility;
107 private boolean expanded;
Eric Erfanian938468d2017-10-24 14:05:52 -0700108 private CharSequence textAfterShow;
109 private int collapseEndAction;
110
yueg81a77ff2017-12-05 10:29:03 -0800111 ViewHolder viewHolder;
yueg6518fdb2018-01-09 17:30:36 -0800112 private AnimatorSet collapseAnimatorSet;
Eric Erfanian938468d2017-10-24 14:05:52 -0700113 private Integer overrideGravity;
yueg87111362017-12-08 12:45:50 -0800114 @VisibleForTesting AnimatorSet exitAnimatorSet;
Eric Erfanian938468d2017-10-24 14:05:52 -0700115
yueg87111362017-12-08 12:45:50 -0800116 private final int primaryIconMoveDistance;
117 private final int leftBoundary;
yueg81a77ff2017-12-05 10:29:03 -0800118 private int savedYPosition = -1;
119
Eric Erfanian938468d2017-10-24 14:05:52 -0700120 /** Type of action after bubble collapse */
121 @Retention(RetentionPolicy.SOURCE)
122 @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE})
123 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
124 public @interface CollapseEnd {
125 int NOTHING = 0;
126 int HIDE = 1;
127 }
128
129 @Retention(RetentionPolicy.SOURCE)
130 @IntDef({Visibility.ENTERING, Visibility.SHOWING, Visibility.EXITING, Visibility.HIDDEN})
131 private @interface Visibility {
132 int HIDDEN = 0;
133 int ENTERING = 1;
134 int SHOWING = 2;
135 int EXITING = 3;
136 }
137
138 /** Indicate bubble expansion state. */
139 @Retention(RetentionPolicy.SOURCE)
140 @IntDef({ExpansionState.START_EXPANDING, ExpansionState.START_COLLAPSING})
141 public @interface ExpansionState {
142 // TODO(yueg): add more states when needed
143 int START_EXPANDING = 0;
144 int START_COLLAPSING = 1;
145 }
146
147 /**
148 * Determines whether bubbles can be shown based on permissions obtained. This should be checked
149 * before attempting to create a Bubble.
150 *
151 * @return true iff bubbles are able to be shown.
152 * @see Settings#canDrawOverlays(Context)
153 */
154 public static boolean canShowBubbles(@NonNull Context context) {
155 return canShowBubblesForTesting != null
156 ? canShowBubblesForTesting
157 : Settings.canDrawOverlays(context);
158 }
159
160 @VisibleForTesting(otherwise = VisibleForTesting.NONE)
161 public static void setCanShowBubblesForTesting(boolean canShowBubbles) {
162 canShowBubblesForTesting = canShowBubbles;
163 }
164
165 /** Returns an Intent to request permission to show overlays */
166 @NonNull
167 public static Intent getRequestPermissionIntent(@NonNull Context context) {
168 return new Intent(
169 Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
170 Uri.fromParts("package", context.getPackageName(), null));
171 }
172
173 /** Creates instances of Bubble. The default implementation just calls the constructor. */
174 @VisibleForTesting
175 public interface BubbleFactory {
176 NewBubble createBubble(@NonNull Context context, @NonNull Handler handler);
177 }
178
179 private static BubbleFactory bubbleFactory = NewBubble::new;
180
181 public static NewBubble createBubble(@NonNull Context context, @NonNull NewBubbleInfo info) {
182 NewBubble bubble = bubbleFactory.createBubble(context, new Handler());
183 bubble.setBubbleInfo(info);
184 return bubble;
185 }
186
187 @VisibleForTesting
188 public static void setBubbleFactory(@NonNull BubbleFactory bubbleFactory) {
189 NewBubble.bubbleFactory = bubbleFactory;
190 }
191
192 @VisibleForTesting
193 public static void resetBubbleFactory() {
194 NewBubble.bubbleFactory = NewBubble::new;
195 }
196
197 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
198 NewBubble(@NonNull Context context, @NonNull Handler handler) {
199 context = new ContextThemeWrapper(context, R.style.Theme_AppCompat);
200 this.context = context;
201 this.handler = handler;
202 windowManager = context.getSystemService(WindowManager.class);
203
204 viewHolder = new ViewHolder(context);
yueg81a77ff2017-12-05 10:29:03 -0800205
206 leftBoundary =
207 context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal)
208 - context
209 .getResources()
210 .getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal);
yueg87111362017-12-08 12:45:50 -0800211 primaryIconMoveDistance =
212 context.getResources().getDimensionPixelSize(R.dimen.bubble_size)
213 - context.getResources().getDimensionPixelSize(R.dimen.bubble_small_icon_size);
Eric Erfanian938468d2017-10-24 14:05:52 -0700214 }
215
216 /** Expands the main bubble menu. */
yuegf539f782017-12-18 16:20:58 -0800217 public void expand() {
yuega235e132017-12-13 14:13:57 -0800218 setPrimaryButtonAccessibilityAction(
219 context.getString(R.string.a11y_bubble_primary_button_collapse_action));
yueg0b4755c2017-12-18 10:01:03 -0800220
yueg81a77ff2017-12-05 10:29:03 -0800221 viewHolder.setDrawerVisibility(View.INVISIBLE);
yueg0b4755c2017-12-18 10:01:03 -0800222 viewHolder.getArrow().setVisibility(View.INVISIBLE);
223 // No click during animation to avoid jank.
224 viewHolder.setPrimaryButtonClickable(false);
225
Eric Erfanian938468d2017-10-24 14:05:52 -0700226 View expandedView = viewHolder.getExpandedView();
227 expandedView
228 .getViewTreeObserver()
229 .addOnPreDrawListener(
230 new OnPreDrawListener() {
231 @Override
232 public boolean onPreDraw() {
yueg81a77ff2017-12-05 10:29:03 -0800233 // Move the whole bubble up so that expanded view is still in screen
234 int moveUpDistance = viewHolder.getMoveUpDistance();
235 if (moveUpDistance != 0) {
236 savedYPosition = windowParams.y;
237 }
238
yueg0b4755c2017-12-18 10:01:03 -0800239 // Animation 1: animate x-move and y-move (if needed) together
yueg81a77ff2017-12-05 10:29:03 -0800240 int deltaX =
241 (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
yueg0b4755c2017-12-18 10:01:03 -0800242 float k = -(float) moveUpDistance / deltaX;
yueg81a77ff2017-12-05 10:29:03 -0800243 if (isDrawingFromRight()) {
244 deltaX = -deltaX;
245 }
yueg0b4755c2017-12-18 10:01:03 -0800246 ValueAnimator xValueAnimator =
247 createBubbleMoveAnimator(
248 windowParams.x - deltaX, windowParams.x, windowParams.y, k);
yueg81a77ff2017-12-05 10:29:03 -0800249
yueg0b4755c2017-12-18 10:01:03 -0800250 // Show expanded view
251 expandedView.setVisibility(View.VISIBLE);
yueg81a77ff2017-12-05 10:29:03 -0800252
yueg0b4755c2017-12-18 10:01:03 -0800253 // Animator 2: reveal expanded view from top left or top right
254 View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
255 ValueAnimator revealAnim =
256 createOpenCloseOutlineProvider(expandedMenu)
257 .createRevealAnimator(expandedMenu, false);
258 revealAnim.setInterpolator(accelerateDecelerateInterpolator);
259
260 // Animator 3: expanded view fade in
261 Animator fadeIn = ObjectAnimator.ofFloat(expandedView, "alpha", 0, 1);
262 fadeIn.setInterpolator(accelerateDecelerateInterpolator);
263
264 // Play all animation together
265 AnimatorSet expandAnimatorSet = new AnimatorSet();
266 expandAnimatorSet.playTogether(revealAnim, fadeIn, xValueAnimator);
267 expandAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
268 expandAnimatorSet.addListener(
yueg87111362017-12-08 12:45:50 -0800269 new AnimatorListenerAdapter() {
yueg81a77ff2017-12-05 10:29:03 -0800270 @Override
271 public void onAnimationEnd(Animator animation) {
yueg0b4755c2017-12-18 10:01:03 -0800272 // Show arrow after animation
273 viewHolder.getArrow().setVisibility(View.VISIBLE);
274 // Safe to click primary button now
275 viewHolder.setPrimaryButtonClickable(true);
yueg81a77ff2017-12-05 10:29:03 -0800276 }
yueg81a77ff2017-12-05 10:29:03 -0800277 });
yueg0b4755c2017-12-18 10:01:03 -0800278 expandAnimatorSet.start();
yueg81a77ff2017-12-05 10:29:03 -0800279
Eric Erfanian938468d2017-10-24 14:05:52 -0700280 expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
Eric Erfanian938468d2017-10-24 14:05:52 -0700281 return false;
282 }
283 });
284 setFocused(true);
285 expanded = true;
286 }
287
yueg81a77ff2017-12-05 10:29:03 -0800288 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
yuegf539f782017-12-18 16:20:58 -0800289 public void startCollapse(@CollapseEnd int endAction, boolean shouldRecoverYPosition) {
yueg81a77ff2017-12-05 10:29:03 -0800290 View expandedView = viewHolder.getExpandedView();
yueg6518fdb2018-01-09 17:30:36 -0800291 if (expandedView.getVisibility() != View.VISIBLE || collapseAnimatorSet != null) {
yueg81a77ff2017-12-05 10:29:03 -0800292 // Drawer is already collapsed or animation is running.
293 return;
294 }
295
296 overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT;
297 setFocused(false);
298
299 if (collapseEndAction == CollapseEnd.NOTHING) {
300 collapseEndAction = endAction;
301 }
yuega235e132017-12-13 14:13:57 -0800302 setPrimaryButtonAccessibilityAction(
303 context.getString(R.string.a11y_bubble_primary_button_expand_action));
yueg0b4755c2017-12-18 10:01:03 -0800304
305 // Hide arrow before animation
306 viewHolder.getArrow().setVisibility(View.INVISIBLE);
307
308 // No click during animation to avoid jank.
309 viewHolder.setPrimaryButtonClickable(false);
310
311 // Calculate animation values
312 int deltaX = (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
313 float k =
314 (savedYPosition != -1 && shouldRecoverYPosition)
315 ? (savedYPosition - windowParams.y) / (float) deltaX
316 : 0;
317 // The position is not useful after collapse
318 savedYPosition = -1;
319
320 // Animation 1: animate x-move and y-move (if needed) together
321 ValueAnimator xValueAnimator =
322 createBubbleMoveAnimator(windowParams.x, windowParams.x - deltaX, windowParams.y, k);
323
324 // Animator 2: hide expanded view to top left or top right
325 View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
326 ValueAnimator revealAnim =
327 createOpenCloseOutlineProvider(expandedMenu).createRevealAnimator(expandedMenu, true);
328 revealAnim.setInterpolator(accelerateDecelerateInterpolator);
329
330 // Animator 3: expanded view fade out
331 Animator fadeOut = ObjectAnimator.ofFloat(expandedView, "alpha", 1, 0);
332 fadeOut.setInterpolator(accelerateDecelerateInterpolator);
333
334 // Play all animation together
yueg6518fdb2018-01-09 17:30:36 -0800335 collapseAnimatorSet = new AnimatorSet();
yueg0b4755c2017-12-18 10:01:03 -0800336 collapseAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
337 collapseAnimatorSet.playTogether(revealAnim, fadeOut, xValueAnimator);
338 collapseAnimatorSet.addListener(
339 new AnimatorListenerAdapter() {
340 @Override
341 public void onAnimationEnd(Animator animation) {
yueg6518fdb2018-01-09 17:30:36 -0800342 collapseAnimatorSet = null;
yueg0b4755c2017-12-18 10:01:03 -0800343 expanded = false;
344
yueg0b4755c2017-12-18 10:01:03 -0800345 // If collapse on the right side, the primary button move left a bit after drawer
346 // visibility becoming GONE. To avoid it, we create a new ViewHolder.
347 // It also set primary button clickable back to true, so no need to reset manually.
348 replaceViewHolder();
349
yueg6518fdb2018-01-09 17:30:36 -0800350 // If this collapse was to come before a hide, do it now.
351 if (collapseEndAction == CollapseEnd.HIDE) {
352 hide();
353 collapseEndAction = CollapseEnd.NOTHING;
354 }
355
yueg0b4755c2017-12-18 10:01:03 -0800356 // Resume normal gravity after any resizing is done.
357 handler.postDelayed(
yueg81a77ff2017-12-05 10:29:03 -0800358 () -> {
yueg0b4755c2017-12-18 10:01:03 -0800359 overrideGravity = null;
360 if (!viewHolder.isMoving()) {
361 viewHolder.undoGravityOverride();
yueg81a77ff2017-12-05 10:29:03 -0800362 }
yueg0b4755c2017-12-18 10:01:03 -0800363 },
364 // Need to wait twice as long for resize and layout
365 WINDOW_REDRAW_DELAY_MILLIS * 2);
366 }
367 });
368 collapseAnimatorSet.start();
yueg81a77ff2017-12-05 10:29:03 -0800369 }
370
Eric Erfanian938468d2017-10-24 14:05:52 -0700371 /**
372 * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is
373 * already showing this method does nothing.
374 */
375 public void show() {
376 if (collapseEndAction == CollapseEnd.HIDE) {
377 // If show() was called while collapsing, make sure we don't hide after.
378 collapseEndAction = CollapseEnd.NOTHING;
379 }
380 if (visibility == Visibility.SHOWING || visibility == Visibility.ENTERING) {
381 return;
382 }
383
yueg07e323c2017-12-19 16:05:47 -0800384 boolean isRtl =
385 TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
Eric Erfanian938468d2017-10-24 14:05:52 -0700386 if (windowParams == null) {
387 // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O.
388 @SuppressWarnings("deprecation")
389 @SuppressLint("InlinedApi")
390 int type =
391 BuildCompat.isAtLeastO()
392 ? LayoutParams.TYPE_APPLICATION_OVERLAY
393 : LayoutParams.TYPE_PHONE;
394
395 windowParams =
396 new LayoutParams(
397 type,
398 LayoutParams.FLAG_NOT_TOUCH_MODAL
399 | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
400 | LayoutParams.FLAG_NOT_FOCUSABLE
401 | LayoutParams.FLAG_LAYOUT_NO_LIMITS,
402 PixelFormat.TRANSLUCENT);
yueg07e323c2017-12-19 16:05:47 -0800403 windowParams.gravity = Gravity.TOP | (isRtl ? Gravity.RIGHT : Gravity.LEFT);
yueg81a77ff2017-12-05 10:29:03 -0800404 windowParams.x = leftBoundary;
Eric Erfanian938468d2017-10-24 14:05:52 -0700405 windowParams.y = currentInfo.getStartingYPosition();
406 windowParams.height = LayoutParams.WRAP_CONTENT;
407 windowParams.width = LayoutParams.WRAP_CONTENT;
408 }
409
yueg87111362017-12-08 12:45:50 -0800410 if (exitAnimatorSet != null) {
411 exitAnimatorSet.removeAllListeners();
412 exitAnimatorSet.cancel();
413 exitAnimatorSet = null;
Eric Erfanian938468d2017-10-24 14:05:52 -0700414 } else {
415 windowManager.addView(viewHolder.getRoot(), windowParams);
yueg87111362017-12-08 12:45:50 -0800416 viewHolder.getPrimaryButton().setVisibility(View.VISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700417 viewHolder.getPrimaryButton().setScaleX(0);
418 viewHolder.getPrimaryButton().setScaleY(0);
yueg87111362017-12-08 12:45:50 -0800419 viewHolder.getPrimaryAvatar().setAlpha(0f);
420 viewHolder.getPrimaryIcon().setAlpha(0f);
yueg07e323c2017-12-19 16:05:47 -0800421 if (isRtl) {
422 onLeftRightSwitch(true);
423 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700424 }
425
426 viewHolder.setChildClickable(true);
427 visibility = Visibility.ENTERING;
yueg87111362017-12-08 12:45:50 -0800428
yuega235e132017-12-13 14:13:57 -0800429 setPrimaryButtonAccessibilityAction(
430 context.getString(R.string.a11y_bubble_primary_button_expand_action));
431
yueg87111362017-12-08 12:45:50 -0800432 // Show bubble animation: scale the whole bubble to 1, and change avatar+icon's alpha to 1
433 ObjectAnimator scaleXAnimator =
434 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 1);
435 ObjectAnimator scaleYAnimator =
436 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 1);
437 ObjectAnimator avatarAlphaAnimator =
438 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 1);
439 ObjectAnimator iconAlphaAnimator =
440 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 1);
441 AnimatorSet enterAnimatorSet = new AnimatorSet();
442 enterAnimatorSet.playTogether(
443 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
444 enterAnimatorSet.setInterpolator(new OvershootInterpolator());
445 enterAnimatorSet.addListener(
446 new AnimatorListenerAdapter() {
447 @Override
448 public void onAnimationEnd(Animator animation) {
449 visibility = Visibility.SHOWING;
450 // Show the queued up text, if available.
451 if (textAfterShow != null) {
452 showText(textAfterShow);
453 textAfterShow = null;
454 }
455 }
456 });
457 enterAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700458
459 updatePrimaryIconAnimation();
460 }
461
462 /** Hide the bubble. */
463 public void hide() {
Eric Erfanian938468d2017-10-24 14:05:52 -0700464 hideHelper(this::defaultAfterHidingAnimation);
465 }
466
467 /** Hide the bubble and reset {@viewHolder} to initial state */
468 public void hideAndReset() {
469 hideHelper(
470 () -> {
471 defaultAfterHidingAnimation();
472 reset();
473 });
474 }
475
476 /** Returns whether the bubble is currently visible */
477 public boolean isVisible() {
478 return visibility == Visibility.SHOWING
479 || visibility == Visibility.ENTERING
480 || visibility == Visibility.EXITING;
481 }
482
483 /**
484 * Set the info for this Bubble to display
485 *
486 * @param bubbleInfo the BubbleInfo to display in this Bubble.
487 */
488 public void setBubbleInfo(@NonNull NewBubbleInfo bubbleInfo) {
489 currentInfo = bubbleInfo;
490 update();
491 }
492
493 /**
494 * Update the state and behavior of actions.
495 *
496 * @param actions the new state of the bubble's actions
497 */
498 public void updateActions(@NonNull List<Action> actions) {
499 currentInfo = NewBubbleInfo.from(currentInfo).setActions(actions).build();
500 updateButtonStates();
501 }
502
yuega5a08d82017-10-31 14:11:53 -0700503 /**
504 * Update the avatar from photo.
505 *
506 * @param avatar the new photo avatar in the bubble's primary button
507 */
508 public void updatePhotoAvatar(@NonNull Drawable avatar) {
509 // Make it round
510 int bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
511 Drawable roundAvatar =
512 DrawableConverter.getRoundedDrawable(context, avatar, bubbleSize, bubbleSize);
513
514 updateAvatar(roundAvatar);
515 }
516
517 /**
518 * Update the avatar.
519 *
520 * @param avatar the new avatar in the bubble's primary button
521 */
522 public void updateAvatar(@NonNull Drawable avatar) {
523 if (!avatar.equals(currentInfo.getAvatar())) {
524 currentInfo = NewBubbleInfo.from(currentInfo).setAvatar(avatar).build();
525 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
526 }
527 }
528
Eric Erfanian938468d2017-10-24 14:05:52 -0700529 /** Returns the currently displayed NewBubbleInfo */
530 public NewBubbleInfo getBubbleInfo() {
531 return currentInfo;
532 }
533
534 /**
yueg6518fdb2018-01-09 17:30:36 -0800535 * Display text. The bubble's drawer is not expandable while text is showing, and the drawer will
536 * be closed if already open.
Eric Erfanian938468d2017-10-24 14:05:52 -0700537 *
538 * @param text the text to display to the user
539 */
540 public void showText(@NonNull CharSequence text) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700541 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800542 startCollapse(CollapseEnd.NOTHING, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700543 }
yueg6518fdb2018-01-09 17:30:36 -0800544 Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
Eric Erfanian938468d2017-10-24 14:05:52 -0700545 }
546
Eric Erfanian938468d2017-10-24 14:05:52 -0700547 @Nullable
548 Integer getGravityOverride() {
549 return overrideGravity;
550 }
551
552 void onMoveStart() {
yueg81a77ff2017-12-05 10:29:03 -0800553 if (viewHolder.getExpandedView().getVisibility() == View.VISIBLE) {
554 viewHolder.setDrawerVisibility(View.INVISIBLE);
555 }
556 expanded = false;
557 savedYPosition = -1;
558
Eric Erfanian938468d2017-10-24 14:05:52 -0700559 viewHolder
560 .getPrimaryButton()
561 .animate()
562 .translationZ(
yuegc6deafc2017-11-06 16:42:13 -0800563 context
564 .getResources()
565 .getDimensionPixelOffset(R.dimen.bubble_dragging_elevation_change));
Eric Erfanian938468d2017-10-24 14:05:52 -0700566 }
567
568 void onMoveFinish() {
569 viewHolder.getPrimaryButton().animate().translationZ(0);
Eric Erfanian938468d2017-10-24 14:05:52 -0700570 }
571
572 void primaryButtonClick() {
Eric Erfanian938468d2017-10-24 14:05:52 -0700573 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800574 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
575 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700576 } else {
yuegf539f782017-12-18 16:20:58 -0800577 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_EXPAND);
578 expand();
Eric Erfanian938468d2017-10-24 14:05:52 -0700579 }
580 }
581
yueg81a77ff2017-12-05 10:29:03 -0800582 void onLeftRightSwitch(boolean onRight) {
yueg87111362017-12-08 12:45:50 -0800583 // Move primary icon to the other side so it's not partially hiden
yueg81a77ff2017-12-05 10:29:03 -0800584 View primaryIcon = viewHolder.getPrimaryIcon();
yueg87111362017-12-08 12:45:50 -0800585 primaryIcon.animate().translationX(onRight ? -primaryIconMoveDistance : 0).start();
yueg81a77ff2017-12-05 10:29:03 -0800586 }
587
Eric Erfanian938468d2017-10-24 14:05:52 -0700588 LayoutParams getWindowParams() {
589 return windowParams;
590 }
591
592 View getRootView() {
593 return viewHolder.getRoot();
594 }
595
596 /**
597 * Hide the bubble if visible. Will run a short exit animation and before hiding, and {@code
598 * afterHiding} after hiding. If the bubble is currently showing text, will hide after the text is
599 * done displaying. If the bubble is not visible this method does nothing.
600 */
yueg87111362017-12-08 12:45:50 -0800601 @VisibleForTesting
602 void hideHelper(Runnable afterHiding) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700603 if (visibility == Visibility.HIDDEN || visibility == Visibility.EXITING) {
604 return;
605 }
606
607 // Make bubble non clickable to prevent further buggy actions
608 viewHolder.setChildClickable(false);
609
yueg6518fdb2018-01-09 17:30:36 -0800610 if (collapseAnimatorSet != null) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700611 collapseEndAction = CollapseEnd.HIDE;
612 return;
613 }
614
615 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800616 startCollapse(CollapseEnd.HIDE, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700617 return;
618 }
619
620 visibility = Visibility.EXITING;
yueg87111362017-12-08 12:45:50 -0800621
622 // Hide bubble animation: scale the whole bubble to 0, and change avatar+icon's alpha to 0
623 ObjectAnimator scaleXAnimator =
624 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 0);
625 ObjectAnimator scaleYAnimator =
626 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 0);
627 ObjectAnimator avatarAlphaAnimator =
628 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 0);
629 ObjectAnimator iconAlphaAnimator =
630 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 0);
631 exitAnimatorSet = new AnimatorSet();
632 exitAnimatorSet.playTogether(
633 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
634 exitAnimatorSet.setInterpolator(new AnticipateInterpolator());
yueg6518fdb2018-01-09 17:30:36 -0800635 exitAnimatorSet.setDuration(HIDE_BUBBLE_ANIMATION_DURATION);
yueg87111362017-12-08 12:45:50 -0800636 exitAnimatorSet.addListener(
637 new AnimatorListenerAdapter() {
638 @Override
yuega235e132017-12-13 14:13:57 -0800639 public void onAnimationStart(Animator animation) {
640 viewHolder.getPrimaryButton().setAccessibilityDelegate(null);
641 }
642
643 @Override
yueg87111362017-12-08 12:45:50 -0800644 public void onAnimationEnd(Animator animation) {
645 afterHiding.run();
646 }
647 });
648 exitAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700649 }
650
651 private void reset() {
652 viewHolder = new ViewHolder(viewHolder.getRoot().getContext());
653 update();
654 }
655
656 private void update() {
yuega5a08d82017-10-31 14:11:53 -0700657 // Whole primary button background
yueg84ac49b2017-11-01 16:22:28 -0700658 Drawable backgroundCirle =
659 context.getResources().getDrawable(R.drawable.bubble_shape_circle, context.getTheme());
Eric Erfanian938468d2017-10-24 14:05:52 -0700660 int primaryTint =
661 ColorUtils.compositeColors(
662 context.getColor(R.color.bubble_primary_background_darken),
663 currentInfo.getPrimaryColor());
yueg84ac49b2017-11-01 16:22:28 -0700664 backgroundCirle.mutate().setTint(primaryTint);
665 viewHolder.getPrimaryButton().setBackground(backgroundCirle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700666
yuega5a08d82017-10-31 14:11:53 -0700667 // Small icon
yueg84ac49b2017-11-01 16:22:28 -0700668 Drawable smallIconBackgroundCircle =
669 context
670 .getResources()
671 .getDrawable(R.drawable.bubble_shape_circle_small, context.getTheme());
672 smallIconBackgroundCircle.setTint(context.getColor(R.color.bubble_button_color_blue));
673 viewHolder.getPrimaryIcon().setBackground(smallIconBackgroundCircle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700674 viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon());
yuega5a08d82017-10-31 14:11:53 -0700675 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
Eric Erfanian938468d2017-10-24 14:05:52 -0700676
yuega5a08d82017-10-31 14:11:53 -0700677 updatePrimaryIconAnimation();
Eric Erfanian938468d2017-10-24 14:05:52 -0700678 updateButtonStates();
679 }
680
681 private void updatePrimaryIconAnimation() {
682 Drawable drawable = viewHolder.getPrimaryIcon().getDrawable();
683 if (drawable instanceof Animatable) {
684 if (isVisible()) {
685 ((Animatable) drawable).start();
686 } else {
687 ((Animatable) drawable).stop();
688 }
689 }
690 }
691
692 private void updateButtonStates() {
yueg84ac49b2017-11-01 16:22:28 -0700693 configureButton(currentInfo.getActions().get(0), viewHolder.getFullScreenButton());
694 configureButton(currentInfo.getActions().get(1), viewHolder.getMuteButton());
695 configureButton(currentInfo.getActions().get(2), viewHolder.getAudioRouteButton());
696 configureButton(currentInfo.getActions().get(3), viewHolder.getEndCallButton());
Eric Erfanian938468d2017-10-24 14:05:52 -0700697 }
698
yueg84ac49b2017-11-01 16:22:28 -0700699 private void configureButton(Action action, NewCheckableButton button) {
yueg07e323c2017-12-19 16:05:47 -0800700 boolean isRtl =
701 TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
702 if (isRtl) {
703 button.setCompoundDrawablesWithIntrinsicBounds(null, null, action.getIconDrawable(), null);
704 } else {
705 button.setCompoundDrawablesWithIntrinsicBounds(action.getIconDrawable(), null, null, null);
706 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700707 button.setChecked(action.isChecked());
708 button.setEnabled(action.isEnabled());
yueg84ac49b2017-11-01 16:22:28 -0700709 button.setText(action.getName());
yuega235e132017-12-13 14:13:57 -0800710 button.setContentDescription(action.getName());
Eric Erfanian938468d2017-10-24 14:05:52 -0700711 button.setOnClickListener(v -> doAction(action));
712 }
713
714 private void doAction(Action action) {
715 try {
716 action.getIntent().send();
717 } catch (CanceledException e) {
718 throw new RuntimeException(e);
719 }
720 }
721
yueg81a77ff2017-12-05 10:29:03 -0800722 /**
723 * Create a new ViewHolder object to replace the old one.It only happens when not moving and
724 * collapsed.
725 */
726 void replaceViewHolder() {
727 LogUtil.enterBlock("NewBubble.replaceViewHolder");
yuegf473e1d2018-01-02 16:23:14 -0800728 // Don't do it. If windowParams is null, either we haven't initialized it or we set it to null.
729 // There is no need to recreate bubble.
730 if (windowParams == null) {
731 return;
732 }
733
Eric Erfanian938468d2017-10-24 14:05:52 -0700734 ViewHolder oldViewHolder = viewHolder;
Eric Erfanian938468d2017-10-24 14:05:52 -0700735
yueg81a77ff2017-12-05 10:29:03 -0800736 // Create a new ViewHolder and copy needed info.
737 viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext());
738 viewHolder
739 .getPrimaryButton()
740 .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild());
yueg87111362017-12-08 12:45:50 -0800741 viewHolder.getPrimaryIcon().setX(isDrawingFromRight() ? 0 : primaryIconMoveDistance);
yueg81a77ff2017-12-05 10:29:03 -0800742 viewHolder
743 .getPrimaryIcon()
yueg87111362017-12-08 12:45:50 -0800744 .setTranslationX(isDrawingFromRight() ? -primaryIconMoveDistance : 0);
yuega235e132017-12-13 14:13:57 -0800745 setPrimaryButtonAccessibilityAction(
746 context.getString(R.string.a11y_bubble_primary_button_expand_action));
Eric Erfanian938468d2017-10-24 14:05:52 -0700747
yueg81a77ff2017-12-05 10:29:03 -0800748 update();
749
750 // Add new view at its horizontal boundary
Eric Erfanian938468d2017-10-24 14:05:52 -0700751 ViewGroup root = viewHolder.getRoot();
yueg81a77ff2017-12-05 10:29:03 -0800752 windowParams.x = leftBoundary;
753 windowParams.gravity = Gravity.TOP | (isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT);
Eric Erfanian938468d2017-10-24 14:05:52 -0700754 windowManager.addView(root, windowParams);
yueg81a77ff2017-12-05 10:29:03 -0800755
756 // Remove the old view after delay
Eric Erfanian938468d2017-10-24 14:05:52 -0700757 root.getViewTreeObserver()
758 .addOnPreDrawListener(
759 new OnPreDrawListener() {
760 @Override
761 public boolean onPreDraw() {
762 root.getViewTreeObserver().removeOnPreDrawListener(this);
763 // Wait a bit before removing the old view; make sure the new one has drawn over it.
764 handler.postDelayed(
765 () -> windowManager.removeView(oldViewHolder.getRoot()),
766 WINDOW_REDRAW_DELAY_MILLIS);
767 return true;
768 }
769 });
770 }
771
yueg81a77ff2017-12-05 10:29:03 -0800772 int getDrawerVisibility() {
773 return viewHolder.getExpandedView().getVisibility();
Eric Erfanian938468d2017-10-24 14:05:52 -0700774 }
775
yuegf473e1d2018-01-02 16:23:14 -0800776 void bottomActionDismiss() {
777 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_DISMISS);
778 // Create bubble at default location at next time
779 hideAndReset();
780 windowParams = null;
781 }
782
783 void bottomActionEndCall() {
784 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_END_CALL);
yueg6518fdb2018-01-09 17:30:36 -0800785 DialerCall call = getCall();
786 if (call != null) {
787 call.disconnect();
788 }
yuegf473e1d2018-01-02 16:23:14 -0800789 }
790
Eric Erfanian938468d2017-10-24 14:05:52 -0700791 private boolean isDrawingFromRight() {
792 return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
793 }
794
795 private void setFocused(boolean focused) {
796 if (focused) {
797 windowParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
798 } else {
799 windowParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
800 }
801 windowManager.updateViewLayout(getRootView(), windowParams);
802 }
803
804 private void defaultAfterHidingAnimation() {
yueg87111362017-12-08 12:45:50 -0800805 exitAnimatorSet = null;
806 viewHolder.getPrimaryButton().setVisibility(View.INVISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700807 windowManager.removeView(viewHolder.getRoot());
808 visibility = Visibility.HIDDEN;
809
810 updatePrimaryIconAnimation();
811 }
812
yueg81a77ff2017-12-05 10:29:03 -0800813 private void logBasicOrCallImpression(DialerImpression.Type impressionType) {
yuegf473e1d2018-01-02 16:23:14 -0800814 DialerCall call = getCall();
yueg81a77ff2017-12-05 10:29:03 -0800815 if (call != null) {
816 Logger.get(context)
817 .logCallImpression(impressionType, call.getUniqueCallId(), call.getTimeAddedMs());
818 } else {
819 Logger.get(context).logImpression(impressionType);
820 }
821 }
822
yuegf473e1d2018-01-02 16:23:14 -0800823 private DialerCall getCall() {
824 // Bubble is shown for outgoing, active or background call
825 DialerCall call = CallList.getInstance().getOutgoingCall();
826 if (call == null) {
827 call = CallList.getInstance().getActiveOrBackgroundCall();
828 }
829 return call;
830 }
831
yuega235e132017-12-13 14:13:57 -0800832 private void setPrimaryButtonAccessibilityAction(String description) {
833 viewHolder
834 .getPrimaryButton()
835 .setAccessibilityDelegate(
836 new AccessibilityDelegate() {
837 @Override
838 public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) {
839 super.onInitializeAccessibilityNodeInfo(v, info);
840
841 AccessibilityAction clickAction =
842 new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, description);
843 info.addAction(clickAction);
844 }
845 });
846 }
847
yueg0b4755c2017-12-18 10:01:03 -0800848 private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider(View view) {
849 int startRectX = isDrawingFromRight() ? view.getMeasuredWidth() : 0;
850 Rect startRect = new Rect(startRectX, 0, startRectX, 0);
851 Rect endRect = new Rect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
852
853 float bubbleRadius = context.getResources().getDimension(R.dimen.bubble_radius);
854 return new RoundedRectRevealOutlineProvider(bubbleRadius, bubbleRadius, startRect, endRect);
855 }
856
857 private ValueAnimator createBubbleMoveAnimator(int startX, int endX, int startY, float k) {
858 ValueAnimator xValueAnimator = ValueAnimator.ofFloat(startX, endX);
859 xValueAnimator.setInterpolator(new LinearOutSlowInInterpolator());
860 xValueAnimator.addUpdateListener(
861 (valueAnimator) -> {
862 // Update windowParams and the root layout.
863 // We can't do ViewPropertyAnimation since it clips children.
864 float newX = (float) valueAnimator.getAnimatedValue();
865 if (k != 0) {
866 windowParams.y = startY + (int) (Math.abs(newX - (float) startX) * k);
867 }
868 windowParams.x = (int) newX;
869 windowManager.updateViewLayout(viewHolder.getRoot(), windowParams);
870 });
871 return xValueAnimator;
872 }
873
Eric Erfanian938468d2017-10-24 14:05:52 -0700874 @VisibleForTesting
875 class ViewHolder {
876
yueg0b4755c2017-12-18 10:01:03 -0800877 private NewMoveHandler moveHandler;
Eric Erfanian938468d2017-10-24 14:05:52 -0700878 private final NewWindowRoot root;
879 private final ViewAnimator primaryButton;
880 private final ImageView primaryIcon;
yuega5a08d82017-10-31 14:11:53 -0700881 private final ImageView primaryAvatar;
yueg0b4755c2017-12-18 10:01:03 -0800882 private final View arrow;
Eric Erfanian938468d2017-10-24 14:05:52 -0700883
884 private final NewCheckableButton fullScreenButton;
885 private final NewCheckableButton muteButton;
886 private final NewCheckableButton audioRouteButton;
887 private final NewCheckableButton endCallButton;
888 private final View expandedView;
889
890 public ViewHolder(Context context) {
891 // Window root is not in the layout file so that the inflater has a view to inflate into
892 this.root = new NewWindowRoot(context);
893 LayoutInflater inflater = LayoutInflater.from(root.getContext());
894 View contentView = inflater.inflate(R.layout.new_bubble_base, root, true);
895 expandedView = contentView.findViewById(R.id.bubble_expanded_layout);
896 primaryButton = contentView.findViewById(R.id.bubble_button_primary);
yuega5a08d82017-10-31 14:11:53 -0700897 primaryAvatar = contentView.findViewById(R.id.bubble_icon_avatar);
Eric Erfanian938468d2017-10-24 14:05:52 -0700898 primaryIcon = contentView.findViewById(R.id.bubble_icon_primary);
yueg0b4755c2017-12-18 10:01:03 -0800899 arrow = contentView.findViewById(R.id.bubble_triangle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700900
901 fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen);
902 muteButton = contentView.findViewById(R.id.bubble_button_mute);
903 audioRouteButton = contentView.findViewById(R.id.bubble_button_audio_route);
904 endCallButton = contentView.findViewById(R.id.bubble_button_end_call);
905
906 root.setOnBackPressedListener(
907 () -> {
908 if (visibility == Visibility.SHOWING && expanded) {
yuegf539f782017-12-18 16:20:58 -0800909 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
910 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700911 return true;
912 }
913 return false;
914 });
915 root.setOnConfigurationChangedListener(
916 (configuration) -> {
yueg6518fdb2018-01-09 17:30:36 -0800917 if (expanded) {
918 // Collapse immediately without animation
919 if (collapseAnimatorSet != null) {
920 collapseAnimatorSet.removeAllListeners();
921 collapseAnimatorSet.cancel();
922 }
923 setDrawerVisibility(View.GONE);
924 expanded = false;
925 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700926 // The values in the current MoveHandler may be stale, so replace it. Then ensure the
927 // Window is in bounds
928 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
929 moveHandler.snapToBounds();
930 });
931 root.setOnTouchListener(
932 (v, event) -> {
933 if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
yuegf539f782017-12-18 16:20:58 -0800934 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
935 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700936 return true;
937 }
938 return false;
939 });
940 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
941 }
942
943 private void setChildClickable(boolean clickable) {
944 fullScreenButton.setClickable(clickable);
945 muteButton.setClickable(clickable);
946 audioRouteButton.setClickable(clickable);
947 endCallButton.setClickable(clickable);
yueg0b4755c2017-12-18 10:01:03 -0800948 setPrimaryButtonClickable(clickable);
949 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700950
yueg0b4755c2017-12-18 10:01:03 -0800951 private void setPrimaryButtonClickable(boolean clickable) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700952 moveHandler.setClickable(clickable);
953 }
954
yueg81a77ff2017-12-05 10:29:03 -0800955 public int getMoveUpDistance() {
956 int deltaAllowed =
957 expandedView.getHeight()
958 - context
959 .getResources()
960 .getDimensionPixelOffset(R.dimen.bubble_button_padding_vertical)
961 * 2;
962 return moveHandler.getMoveUpDistance(deltaAllowed);
963 }
964
Eric Erfanian938468d2017-10-24 14:05:52 -0700965 public ViewGroup getRoot() {
966 return root;
967 }
968
969 public ViewAnimator getPrimaryButton() {
970 return primaryButton;
971 }
972
973 public ImageView getPrimaryIcon() {
974 return primaryIcon;
975 }
976
yuega5a08d82017-10-31 14:11:53 -0700977 public ImageView getPrimaryAvatar() {
978 return primaryAvatar;
979 }
980
Eric Erfanian938468d2017-10-24 14:05:52 -0700981 public View getExpandedView() {
982 return expandedView;
983 }
984
yueg0b4755c2017-12-18 10:01:03 -0800985 public View getArrow() {
986 return arrow;
987 }
988
Eric Erfanian938468d2017-10-24 14:05:52 -0700989 public NewCheckableButton getFullScreenButton() {
990 return fullScreenButton;
991 }
992
993 public NewCheckableButton getMuteButton() {
994 return muteButton;
995 }
996
997 public NewCheckableButton getAudioRouteButton() {
998 return audioRouteButton;
999 }
1000
1001 public NewCheckableButton getEndCallButton() {
1002 return endCallButton;
1003 }
1004
1005 public void setDrawerVisibility(int visibility) {
1006 expandedView.setVisibility(visibility);
1007 }
1008
1009 public boolean isMoving() {
1010 return moveHandler.isMoving();
1011 }
1012
1013 public void undoGravityOverride() {
1014 moveHandler.undoGravityOverride();
1015 }
1016 }
Eric Erfanian938468d2017-10-24 14:05:52 -07001017}