blob: cb7a094691e666459e90e849052a5ecd0fb10a9b [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
yueg2f664032018-01-10 11:30:30 -0800384 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_SHOW);
385
yueg07e323c2017-12-19 16:05:47 -0800386 boolean isRtl =
387 TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
Eric Erfanian938468d2017-10-24 14:05:52 -0700388 if (windowParams == null) {
389 // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O.
390 @SuppressWarnings("deprecation")
391 @SuppressLint("InlinedApi")
392 int type =
393 BuildCompat.isAtLeastO()
394 ? LayoutParams.TYPE_APPLICATION_OVERLAY
395 : LayoutParams.TYPE_PHONE;
396
397 windowParams =
398 new LayoutParams(
399 type,
400 LayoutParams.FLAG_NOT_TOUCH_MODAL
401 | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
402 | LayoutParams.FLAG_NOT_FOCUSABLE
403 | LayoutParams.FLAG_LAYOUT_NO_LIMITS,
404 PixelFormat.TRANSLUCENT);
yueg07e323c2017-12-19 16:05:47 -0800405 windowParams.gravity = Gravity.TOP | (isRtl ? Gravity.RIGHT : Gravity.LEFT);
yueg81a77ff2017-12-05 10:29:03 -0800406 windowParams.x = leftBoundary;
Eric Erfanian938468d2017-10-24 14:05:52 -0700407 windowParams.y = currentInfo.getStartingYPosition();
408 windowParams.height = LayoutParams.WRAP_CONTENT;
409 windowParams.width = LayoutParams.WRAP_CONTENT;
410 }
411
yueg87111362017-12-08 12:45:50 -0800412 if (exitAnimatorSet != null) {
413 exitAnimatorSet.removeAllListeners();
414 exitAnimatorSet.cancel();
415 exitAnimatorSet = null;
Eric Erfanian938468d2017-10-24 14:05:52 -0700416 } else {
417 windowManager.addView(viewHolder.getRoot(), windowParams);
yueg87111362017-12-08 12:45:50 -0800418 viewHolder.getPrimaryButton().setVisibility(View.VISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700419 viewHolder.getPrimaryButton().setScaleX(0);
420 viewHolder.getPrimaryButton().setScaleY(0);
yueg87111362017-12-08 12:45:50 -0800421 viewHolder.getPrimaryAvatar().setAlpha(0f);
422 viewHolder.getPrimaryIcon().setAlpha(0f);
yueg07e323c2017-12-19 16:05:47 -0800423 if (isRtl) {
424 onLeftRightSwitch(true);
425 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700426 }
427
428 viewHolder.setChildClickable(true);
429 visibility = Visibility.ENTERING;
yueg87111362017-12-08 12:45:50 -0800430
yuega235e132017-12-13 14:13:57 -0800431 setPrimaryButtonAccessibilityAction(
432 context.getString(R.string.a11y_bubble_primary_button_expand_action));
433
yueg87111362017-12-08 12:45:50 -0800434 // Show bubble animation: scale the whole bubble to 1, and change avatar+icon's alpha to 1
435 ObjectAnimator scaleXAnimator =
436 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 1);
437 ObjectAnimator scaleYAnimator =
438 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 1);
439 ObjectAnimator avatarAlphaAnimator =
440 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 1);
441 ObjectAnimator iconAlphaAnimator =
442 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 1);
443 AnimatorSet enterAnimatorSet = new AnimatorSet();
444 enterAnimatorSet.playTogether(
445 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
446 enterAnimatorSet.setInterpolator(new OvershootInterpolator());
447 enterAnimatorSet.addListener(
448 new AnimatorListenerAdapter() {
449 @Override
450 public void onAnimationEnd(Animator animation) {
451 visibility = Visibility.SHOWING;
452 // Show the queued up text, if available.
453 if (textAfterShow != null) {
454 showText(textAfterShow);
455 textAfterShow = null;
456 }
457 }
458 });
459 enterAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700460
461 updatePrimaryIconAnimation();
462 }
463
464 /** Hide the bubble. */
465 public void hide() {
Eric Erfanian938468d2017-10-24 14:05:52 -0700466 hideHelper(this::defaultAfterHidingAnimation);
467 }
468
469 /** Hide the bubble and reset {@viewHolder} to initial state */
470 public void hideAndReset() {
471 hideHelper(
472 () -> {
473 defaultAfterHidingAnimation();
474 reset();
475 });
476 }
477
478 /** Returns whether the bubble is currently visible */
479 public boolean isVisible() {
480 return visibility == Visibility.SHOWING
481 || visibility == Visibility.ENTERING
482 || visibility == Visibility.EXITING;
483 }
484
485 /**
486 * Set the info for this Bubble to display
487 *
488 * @param bubbleInfo the BubbleInfo to display in this Bubble.
489 */
490 public void setBubbleInfo(@NonNull NewBubbleInfo bubbleInfo) {
491 currentInfo = bubbleInfo;
492 update();
493 }
494
495 /**
496 * Update the state and behavior of actions.
497 *
498 * @param actions the new state of the bubble's actions
499 */
500 public void updateActions(@NonNull List<Action> actions) {
501 currentInfo = NewBubbleInfo.from(currentInfo).setActions(actions).build();
502 updateButtonStates();
503 }
504
yuega5a08d82017-10-31 14:11:53 -0700505 /**
506 * Update the avatar from photo.
507 *
508 * @param avatar the new photo avatar in the bubble's primary button
509 */
510 public void updatePhotoAvatar(@NonNull Drawable avatar) {
511 // Make it round
512 int bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
513 Drawable roundAvatar =
514 DrawableConverter.getRoundedDrawable(context, avatar, bubbleSize, bubbleSize);
515
516 updateAvatar(roundAvatar);
517 }
518
519 /**
520 * Update the avatar.
521 *
522 * @param avatar the new avatar in the bubble's primary button
523 */
524 public void updateAvatar(@NonNull Drawable avatar) {
525 if (!avatar.equals(currentInfo.getAvatar())) {
526 currentInfo = NewBubbleInfo.from(currentInfo).setAvatar(avatar).build();
527 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
528 }
529 }
530
Eric Erfanian938468d2017-10-24 14:05:52 -0700531 /** Returns the currently displayed NewBubbleInfo */
532 public NewBubbleInfo getBubbleInfo() {
533 return currentInfo;
534 }
535
536 /**
yueg6518fdb2018-01-09 17:30:36 -0800537 * Display text. The bubble's drawer is not expandable while text is showing, and the drawer will
538 * be closed if already open.
Eric Erfanian938468d2017-10-24 14:05:52 -0700539 *
540 * @param text the text to display to the user
541 */
542 public void showText(@NonNull CharSequence text) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700543 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800544 startCollapse(CollapseEnd.NOTHING, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700545 }
yueg6518fdb2018-01-09 17:30:36 -0800546 Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
Eric Erfanian938468d2017-10-24 14:05:52 -0700547 }
548
Eric Erfanian938468d2017-10-24 14:05:52 -0700549 @Nullable
550 Integer getGravityOverride() {
551 return overrideGravity;
552 }
553
554 void onMoveStart() {
yueg81a77ff2017-12-05 10:29:03 -0800555 if (viewHolder.getExpandedView().getVisibility() == View.VISIBLE) {
556 viewHolder.setDrawerVisibility(View.INVISIBLE);
557 }
558 expanded = false;
559 savedYPosition = -1;
560
Eric Erfanian938468d2017-10-24 14:05:52 -0700561 viewHolder
562 .getPrimaryButton()
563 .animate()
564 .translationZ(
yuegc6deafc2017-11-06 16:42:13 -0800565 context
566 .getResources()
567 .getDimensionPixelOffset(R.dimen.bubble_dragging_elevation_change));
Eric Erfanian938468d2017-10-24 14:05:52 -0700568 }
569
570 void onMoveFinish() {
571 viewHolder.getPrimaryButton().animate().translationZ(0);
Eric Erfanian938468d2017-10-24 14:05:52 -0700572 }
573
574 void primaryButtonClick() {
Eric Erfanian938468d2017-10-24 14:05:52 -0700575 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800576 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
577 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700578 } else {
yuegf539f782017-12-18 16:20:58 -0800579 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_EXPAND);
580 expand();
Eric Erfanian938468d2017-10-24 14:05:52 -0700581 }
582 }
583
yueg81a77ff2017-12-05 10:29:03 -0800584 void onLeftRightSwitch(boolean onRight) {
yueg87111362017-12-08 12:45:50 -0800585 // Move primary icon to the other side so it's not partially hiden
yueg81a77ff2017-12-05 10:29:03 -0800586 View primaryIcon = viewHolder.getPrimaryIcon();
yueg87111362017-12-08 12:45:50 -0800587 primaryIcon.animate().translationX(onRight ? -primaryIconMoveDistance : 0).start();
yueg81a77ff2017-12-05 10:29:03 -0800588 }
589
Eric Erfanian938468d2017-10-24 14:05:52 -0700590 LayoutParams getWindowParams() {
591 return windowParams;
592 }
593
594 View getRootView() {
595 return viewHolder.getRoot();
596 }
597
598 /**
599 * Hide the bubble if visible. Will run a short exit animation and before hiding, and {@code
600 * afterHiding} after hiding. If the bubble is currently showing text, will hide after the text is
601 * done displaying. If the bubble is not visible this method does nothing.
602 */
yueg87111362017-12-08 12:45:50 -0800603 @VisibleForTesting
604 void hideHelper(Runnable afterHiding) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700605 if (visibility == Visibility.HIDDEN || visibility == Visibility.EXITING) {
606 return;
607 }
608
609 // Make bubble non clickable to prevent further buggy actions
610 viewHolder.setChildClickable(false);
611
yueg6518fdb2018-01-09 17:30:36 -0800612 if (collapseAnimatorSet != null) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700613 collapseEndAction = CollapseEnd.HIDE;
614 return;
615 }
616
617 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800618 startCollapse(CollapseEnd.HIDE, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700619 return;
620 }
621
622 visibility = Visibility.EXITING;
yueg87111362017-12-08 12:45:50 -0800623
624 // Hide bubble animation: scale the whole bubble to 0, and change avatar+icon's alpha to 0
625 ObjectAnimator scaleXAnimator =
626 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 0);
627 ObjectAnimator scaleYAnimator =
628 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 0);
629 ObjectAnimator avatarAlphaAnimator =
630 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 0);
631 ObjectAnimator iconAlphaAnimator =
632 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 0);
633 exitAnimatorSet = new AnimatorSet();
634 exitAnimatorSet.playTogether(
635 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
636 exitAnimatorSet.setInterpolator(new AnticipateInterpolator());
yueg6518fdb2018-01-09 17:30:36 -0800637 exitAnimatorSet.setDuration(HIDE_BUBBLE_ANIMATION_DURATION);
yueg87111362017-12-08 12:45:50 -0800638 exitAnimatorSet.addListener(
639 new AnimatorListenerAdapter() {
640 @Override
yuega235e132017-12-13 14:13:57 -0800641 public void onAnimationStart(Animator animation) {
642 viewHolder.getPrimaryButton().setAccessibilityDelegate(null);
643 }
644
645 @Override
yueg87111362017-12-08 12:45:50 -0800646 public void onAnimationEnd(Animator animation) {
647 afterHiding.run();
648 }
649 });
650 exitAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700651 }
652
653 private void reset() {
654 viewHolder = new ViewHolder(viewHolder.getRoot().getContext());
655 update();
656 }
657
658 private void update() {
yuega5a08d82017-10-31 14:11:53 -0700659 // Whole primary button background
yueg84ac49b2017-11-01 16:22:28 -0700660 Drawable backgroundCirle =
661 context.getResources().getDrawable(R.drawable.bubble_shape_circle, context.getTheme());
Eric Erfanian938468d2017-10-24 14:05:52 -0700662 int primaryTint =
663 ColorUtils.compositeColors(
664 context.getColor(R.color.bubble_primary_background_darken),
665 currentInfo.getPrimaryColor());
yueg84ac49b2017-11-01 16:22:28 -0700666 backgroundCirle.mutate().setTint(primaryTint);
667 viewHolder.getPrimaryButton().setBackground(backgroundCirle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700668
yuega5a08d82017-10-31 14:11:53 -0700669 // Small icon
yueg84ac49b2017-11-01 16:22:28 -0700670 Drawable smallIconBackgroundCircle =
671 context
672 .getResources()
673 .getDrawable(R.drawable.bubble_shape_circle_small, context.getTheme());
674 smallIconBackgroundCircle.setTint(context.getColor(R.color.bubble_button_color_blue));
675 viewHolder.getPrimaryIcon().setBackground(smallIconBackgroundCircle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700676 viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon());
yuega5a08d82017-10-31 14:11:53 -0700677 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
Eric Erfanian938468d2017-10-24 14:05:52 -0700678
yuega5a08d82017-10-31 14:11:53 -0700679 updatePrimaryIconAnimation();
Eric Erfanian938468d2017-10-24 14:05:52 -0700680 updateButtonStates();
681 }
682
683 private void updatePrimaryIconAnimation() {
684 Drawable drawable = viewHolder.getPrimaryIcon().getDrawable();
685 if (drawable instanceof Animatable) {
686 if (isVisible()) {
687 ((Animatable) drawable).start();
688 } else {
689 ((Animatable) drawable).stop();
690 }
691 }
692 }
693
694 private void updateButtonStates() {
yueg84ac49b2017-11-01 16:22:28 -0700695 configureButton(currentInfo.getActions().get(0), viewHolder.getFullScreenButton());
696 configureButton(currentInfo.getActions().get(1), viewHolder.getMuteButton());
697 configureButton(currentInfo.getActions().get(2), viewHolder.getAudioRouteButton());
698 configureButton(currentInfo.getActions().get(3), viewHolder.getEndCallButton());
Eric Erfanian938468d2017-10-24 14:05:52 -0700699 }
700
yueg84ac49b2017-11-01 16:22:28 -0700701 private void configureButton(Action action, NewCheckableButton button) {
yueg07e323c2017-12-19 16:05:47 -0800702 boolean isRtl =
703 TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
704 if (isRtl) {
705 button.setCompoundDrawablesWithIntrinsicBounds(null, null, action.getIconDrawable(), null);
706 } else {
707 button.setCompoundDrawablesWithIntrinsicBounds(action.getIconDrawable(), null, null, null);
708 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700709 button.setChecked(action.isChecked());
710 button.setEnabled(action.isEnabled());
yueg84ac49b2017-11-01 16:22:28 -0700711 button.setText(action.getName());
yuega235e132017-12-13 14:13:57 -0800712 button.setContentDescription(action.getName());
Eric Erfanian938468d2017-10-24 14:05:52 -0700713 button.setOnClickListener(v -> doAction(action));
714 }
715
716 private void doAction(Action action) {
717 try {
718 action.getIntent().send();
719 } catch (CanceledException e) {
720 throw new RuntimeException(e);
721 }
722 }
723
yueg81a77ff2017-12-05 10:29:03 -0800724 /**
725 * Create a new ViewHolder object to replace the old one.It only happens when not moving and
726 * collapsed.
727 */
728 void replaceViewHolder() {
729 LogUtil.enterBlock("NewBubble.replaceViewHolder");
yuegf473e1d2018-01-02 16:23:14 -0800730 // Don't do it. If windowParams is null, either we haven't initialized it or we set it to null.
731 // There is no need to recreate bubble.
732 if (windowParams == null) {
733 return;
734 }
735
Eric Erfanian938468d2017-10-24 14:05:52 -0700736 ViewHolder oldViewHolder = viewHolder;
Eric Erfanian938468d2017-10-24 14:05:52 -0700737
yueg81a77ff2017-12-05 10:29:03 -0800738 // Create a new ViewHolder and copy needed info.
739 viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext());
740 viewHolder
741 .getPrimaryButton()
742 .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild());
yueg87111362017-12-08 12:45:50 -0800743 viewHolder.getPrimaryIcon().setX(isDrawingFromRight() ? 0 : primaryIconMoveDistance);
yueg81a77ff2017-12-05 10:29:03 -0800744 viewHolder
745 .getPrimaryIcon()
yueg87111362017-12-08 12:45:50 -0800746 .setTranslationX(isDrawingFromRight() ? -primaryIconMoveDistance : 0);
yuega235e132017-12-13 14:13:57 -0800747 setPrimaryButtonAccessibilityAction(
748 context.getString(R.string.a11y_bubble_primary_button_expand_action));
Eric Erfanian938468d2017-10-24 14:05:52 -0700749
yueg81a77ff2017-12-05 10:29:03 -0800750 update();
751
752 // Add new view at its horizontal boundary
Eric Erfanian938468d2017-10-24 14:05:52 -0700753 ViewGroup root = viewHolder.getRoot();
yueg81a77ff2017-12-05 10:29:03 -0800754 windowParams.x = leftBoundary;
755 windowParams.gravity = Gravity.TOP | (isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT);
Eric Erfanian938468d2017-10-24 14:05:52 -0700756 windowManager.addView(root, windowParams);
yueg81a77ff2017-12-05 10:29:03 -0800757
758 // Remove the old view after delay
Eric Erfanian938468d2017-10-24 14:05:52 -0700759 root.getViewTreeObserver()
760 .addOnPreDrawListener(
761 new OnPreDrawListener() {
762 @Override
763 public boolean onPreDraw() {
764 root.getViewTreeObserver().removeOnPreDrawListener(this);
765 // Wait a bit before removing the old view; make sure the new one has drawn over it.
766 handler.postDelayed(
767 () -> windowManager.removeView(oldViewHolder.getRoot()),
768 WINDOW_REDRAW_DELAY_MILLIS);
769 return true;
770 }
771 });
772 }
773
yueg81a77ff2017-12-05 10:29:03 -0800774 int getDrawerVisibility() {
775 return viewHolder.getExpandedView().getVisibility();
Eric Erfanian938468d2017-10-24 14:05:52 -0700776 }
777
yuegf473e1d2018-01-02 16:23:14 -0800778 void bottomActionDismiss() {
779 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_DISMISS);
780 // Create bubble at default location at next time
781 hideAndReset();
782 windowParams = null;
783 }
784
785 void bottomActionEndCall() {
786 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_END_CALL);
yueg6518fdb2018-01-09 17:30:36 -0800787 DialerCall call = getCall();
788 if (call != null) {
789 call.disconnect();
790 }
yuegf473e1d2018-01-02 16:23:14 -0800791 }
792
Eric Erfanian938468d2017-10-24 14:05:52 -0700793 private boolean isDrawingFromRight() {
794 return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
795 }
796
797 private void setFocused(boolean focused) {
798 if (focused) {
799 windowParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
800 } else {
801 windowParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
802 }
803 windowManager.updateViewLayout(getRootView(), windowParams);
804 }
805
806 private void defaultAfterHidingAnimation() {
yueg87111362017-12-08 12:45:50 -0800807 exitAnimatorSet = null;
808 viewHolder.getPrimaryButton().setVisibility(View.INVISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700809 windowManager.removeView(viewHolder.getRoot());
810 visibility = Visibility.HIDDEN;
811
812 updatePrimaryIconAnimation();
813 }
814
yueg81a77ff2017-12-05 10:29:03 -0800815 private void logBasicOrCallImpression(DialerImpression.Type impressionType) {
yuegf473e1d2018-01-02 16:23:14 -0800816 DialerCall call = getCall();
yueg81a77ff2017-12-05 10:29:03 -0800817 if (call != null) {
818 Logger.get(context)
819 .logCallImpression(impressionType, call.getUniqueCallId(), call.getTimeAddedMs());
820 } else {
821 Logger.get(context).logImpression(impressionType);
822 }
823 }
824
yuegf473e1d2018-01-02 16:23:14 -0800825 private DialerCall getCall() {
826 // Bubble is shown for outgoing, active or background call
827 DialerCall call = CallList.getInstance().getOutgoingCall();
828 if (call == null) {
829 call = CallList.getInstance().getActiveOrBackgroundCall();
830 }
831 return call;
832 }
833
yuega235e132017-12-13 14:13:57 -0800834 private void setPrimaryButtonAccessibilityAction(String description) {
835 viewHolder
836 .getPrimaryButton()
837 .setAccessibilityDelegate(
838 new AccessibilityDelegate() {
839 @Override
840 public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) {
841 super.onInitializeAccessibilityNodeInfo(v, info);
842
843 AccessibilityAction clickAction =
844 new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, description);
845 info.addAction(clickAction);
846 }
847 });
848 }
849
yueg0b4755c2017-12-18 10:01:03 -0800850 private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider(View view) {
851 int startRectX = isDrawingFromRight() ? view.getMeasuredWidth() : 0;
852 Rect startRect = new Rect(startRectX, 0, startRectX, 0);
853 Rect endRect = new Rect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
854
855 float bubbleRadius = context.getResources().getDimension(R.dimen.bubble_radius);
856 return new RoundedRectRevealOutlineProvider(bubbleRadius, bubbleRadius, startRect, endRect);
857 }
858
859 private ValueAnimator createBubbleMoveAnimator(int startX, int endX, int startY, float k) {
860 ValueAnimator xValueAnimator = ValueAnimator.ofFloat(startX, endX);
861 xValueAnimator.setInterpolator(new LinearOutSlowInInterpolator());
862 xValueAnimator.addUpdateListener(
863 (valueAnimator) -> {
864 // Update windowParams and the root layout.
865 // We can't do ViewPropertyAnimation since it clips children.
866 float newX = (float) valueAnimator.getAnimatedValue();
867 if (k != 0) {
868 windowParams.y = startY + (int) (Math.abs(newX - (float) startX) * k);
869 }
870 windowParams.x = (int) newX;
871 windowManager.updateViewLayout(viewHolder.getRoot(), windowParams);
872 });
873 return xValueAnimator;
874 }
875
Eric Erfanian938468d2017-10-24 14:05:52 -0700876 @VisibleForTesting
877 class ViewHolder {
878
yueg0b4755c2017-12-18 10:01:03 -0800879 private NewMoveHandler moveHandler;
Eric Erfanian938468d2017-10-24 14:05:52 -0700880 private final NewWindowRoot root;
881 private final ViewAnimator primaryButton;
882 private final ImageView primaryIcon;
yuega5a08d82017-10-31 14:11:53 -0700883 private final ImageView primaryAvatar;
yueg0b4755c2017-12-18 10:01:03 -0800884 private final View arrow;
Eric Erfanian938468d2017-10-24 14:05:52 -0700885
886 private final NewCheckableButton fullScreenButton;
887 private final NewCheckableButton muteButton;
888 private final NewCheckableButton audioRouteButton;
889 private final NewCheckableButton endCallButton;
890 private final View expandedView;
891
892 public ViewHolder(Context context) {
893 // Window root is not in the layout file so that the inflater has a view to inflate into
894 this.root = new NewWindowRoot(context);
895 LayoutInflater inflater = LayoutInflater.from(root.getContext());
896 View contentView = inflater.inflate(R.layout.new_bubble_base, root, true);
897 expandedView = contentView.findViewById(R.id.bubble_expanded_layout);
898 primaryButton = contentView.findViewById(R.id.bubble_button_primary);
yuega5a08d82017-10-31 14:11:53 -0700899 primaryAvatar = contentView.findViewById(R.id.bubble_icon_avatar);
Eric Erfanian938468d2017-10-24 14:05:52 -0700900 primaryIcon = contentView.findViewById(R.id.bubble_icon_primary);
yueg0b4755c2017-12-18 10:01:03 -0800901 arrow = contentView.findViewById(R.id.bubble_triangle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700902
903 fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen);
904 muteButton = contentView.findViewById(R.id.bubble_button_mute);
905 audioRouteButton = contentView.findViewById(R.id.bubble_button_audio_route);
906 endCallButton = contentView.findViewById(R.id.bubble_button_end_call);
907
908 root.setOnBackPressedListener(
909 () -> {
910 if (visibility == Visibility.SHOWING && expanded) {
yuegf539f782017-12-18 16:20:58 -0800911 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
912 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700913 return true;
914 }
915 return false;
916 });
917 root.setOnConfigurationChangedListener(
918 (configuration) -> {
yueg6518fdb2018-01-09 17:30:36 -0800919 if (expanded) {
920 // Collapse immediately without animation
921 if (collapseAnimatorSet != null) {
922 collapseAnimatorSet.removeAllListeners();
923 collapseAnimatorSet.cancel();
924 }
925 setDrawerVisibility(View.GONE);
926 expanded = false;
927 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700928 // The values in the current MoveHandler may be stale, so replace it. Then ensure the
929 // Window is in bounds
930 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
931 moveHandler.snapToBounds();
932 });
933 root.setOnTouchListener(
934 (v, event) -> {
935 if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
yuegf539f782017-12-18 16:20:58 -0800936 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
937 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700938 return true;
939 }
940 return false;
941 });
942 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
943 }
944
945 private void setChildClickable(boolean clickable) {
946 fullScreenButton.setClickable(clickable);
947 muteButton.setClickable(clickable);
948 audioRouteButton.setClickable(clickable);
949 endCallButton.setClickable(clickable);
yueg0b4755c2017-12-18 10:01:03 -0800950 setPrimaryButtonClickable(clickable);
951 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700952
yueg0b4755c2017-12-18 10:01:03 -0800953 private void setPrimaryButtonClickable(boolean clickable) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700954 moveHandler.setClickable(clickable);
955 }
956
yueg81a77ff2017-12-05 10:29:03 -0800957 public int getMoveUpDistance() {
958 int deltaAllowed =
959 expandedView.getHeight()
960 - context
961 .getResources()
962 .getDimensionPixelOffset(R.dimen.bubble_button_padding_vertical)
963 * 2;
964 return moveHandler.getMoveUpDistance(deltaAllowed);
965 }
966
Eric Erfanian938468d2017-10-24 14:05:52 -0700967 public ViewGroup getRoot() {
968 return root;
969 }
970
971 public ViewAnimator getPrimaryButton() {
972 return primaryButton;
973 }
974
975 public ImageView getPrimaryIcon() {
976 return primaryIcon;
977 }
978
yuega5a08d82017-10-31 14:11:53 -0700979 public ImageView getPrimaryAvatar() {
980 return primaryAvatar;
981 }
982
Eric Erfanian938468d2017-10-24 14:05:52 -0700983 public View getExpandedView() {
984 return expandedView;
985 }
986
yueg0b4755c2017-12-18 10:01:03 -0800987 public View getArrow() {
988 return arrow;
989 }
990
Eric Erfanian938468d2017-10-24 14:05:52 -0700991 public NewCheckableButton getFullScreenButton() {
992 return fullScreenButton;
993 }
994
995 public NewCheckableButton getMuteButton() {
996 return muteButton;
997 }
998
999 public NewCheckableButton getAudioRouteButton() {
1000 return audioRouteButton;
1001 }
1002
1003 public NewCheckableButton getEndCallButton() {
1004 return endCallButton;
1005 }
1006
1007 public void setDrawerVisibility(int visibility) {
1008 expandedView.setVisibility(visibility);
1009 }
1010
1011 public boolean isMoving() {
1012 return moveHandler.isMoving();
1013 }
1014
1015 public void undoGravityOverride() {
1016 moveHandler.undoGravityOverride();
1017 }
1018 }
Eric Erfanian938468d2017-10-24 14:05:52 -07001019}