blob: f5a036f932e5b1c62bb93fe776009903c13117ef [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.transition.TransitionManager;
44import android.transition.TransitionValues;
45import android.view.ContextThemeWrapper;
46import android.view.Gravity;
47import android.view.LayoutInflater;
48import android.view.MotionEvent;
49import android.view.View;
yuega235e132017-12-13 14:13:57 -080050import android.view.View.AccessibilityDelegate;
Eric Erfanian938468d2017-10-24 14:05:52 -070051import android.view.ViewGroup;
52import android.view.ViewPropertyAnimator;
53import android.view.ViewTreeObserver.OnPreDrawListener;
54import android.view.WindowManager;
55import android.view.WindowManager.LayoutParams;
yuega235e132017-12-13 14:13:57 -080056import android.view.accessibility.AccessibilityNodeInfo;
57import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
yueg0b4755c2017-12-18 10:01:03 -080058import android.view.animation.AccelerateDecelerateInterpolator;
Eric Erfanian938468d2017-10-24 14:05:52 -070059import android.view.animation.AnticipateInterpolator;
60import android.view.animation.OvershootInterpolator;
61import android.widget.ImageView;
62import android.widget.TextView;
yuegf473e1d2018-01-02 16:23:14 -080063import android.widget.Toast;
Eric Erfanian938468d2017-10-24 14:05:52 -070064import android.widget.ViewAnimator;
yueg81a77ff2017-12-05 10:29:03 -080065import com.android.dialer.common.LogUtil;
66import com.android.dialer.logging.DialerImpression;
67import com.android.dialer.logging.Logger;
yuega5a08d82017-10-31 14:11:53 -070068import com.android.dialer.util.DrawableConverter;
yueg81a77ff2017-12-05 10:29:03 -080069import com.android.incallui.call.CallList;
70import com.android.incallui.call.DialerCall;
Eric Erfanian938468d2017-10-24 14:05:52 -070071import com.android.newbubble.NewBubbleInfo.Action;
72import java.lang.annotation.Retention;
73import java.lang.annotation.RetentionPolicy;
74import java.util.List;
yueg07e323c2017-12-19 16:05:47 -080075import java.util.Locale;
Eric Erfanian938468d2017-10-24 14:05:52 -070076
77/**
78 * Creates and manages a bubble window from information in a {@link NewBubbleInfo}. Before creating,
79 * be sure to check whether bubbles may be shown using {@link #canShowBubbles(Context)} and request
80 * permission if necessary ({@link #getRequestPermissionIntent(Context)} is provided for
81 * convenience)
82 */
83public class NewBubble {
84 // This class has some odd behavior that is not immediately obvious in order to avoid jank when
85 // resizing. See http://go/bubble-resize for details.
86
87 // How long text should show after showText(CharSequence) is called
88 private static final int SHOW_TEXT_DURATION_MILLIS = 3000;
89 // How long the new window should show before destroying the old one during resize operations.
90 // This ensures the new window has had time to draw first.
91 private static final int WINDOW_REDRAW_DELAY_MILLIS = 50;
92
yueg0b4755c2017-12-18 10:01:03 -080093 private static final int EXPAND_AND_COLLAPSE_ANIMATION_DURATION = 200;
94
Eric Erfanian938468d2017-10-24 14:05:52 -070095 private static Boolean canShowBubblesForTesting = null;
96
yueg0b4755c2017-12-18 10:01:03 -080097 private final AccelerateDecelerateInterpolator accelerateDecelerateInterpolator =
98 new AccelerateDecelerateInterpolator();
99
Eric Erfanian938468d2017-10-24 14:05:52 -0700100 private final Context context;
101 private final WindowManager windowManager;
102
103 private final Handler handler;
104 private LayoutParams windowParams;
105
106 // Initialized in factory method
107 @SuppressWarnings("NullableProblems")
108 @NonNull
109 private NewBubbleInfo currentInfo;
110
111 @Visibility private int visibility;
112 private boolean expanded;
113 private boolean textShowing;
114 private boolean hideAfterText;
115 private CharSequence textAfterShow;
116 private int collapseEndAction;
117
yueg81a77ff2017-12-05 10:29:03 -0800118 ViewHolder viewHolder;
Eric Erfanian938468d2017-10-24 14:05:52 -0700119 private ViewPropertyAnimator collapseAnimation;
120 private Integer overrideGravity;
yueg87111362017-12-08 12:45:50 -0800121 @VisibleForTesting AnimatorSet exitAnimatorSet;
Eric Erfanian938468d2017-10-24 14:05:52 -0700122
yueg87111362017-12-08 12:45:50 -0800123 private final int primaryIconMoveDistance;
124 private final int leftBoundary;
yueg81a77ff2017-12-05 10:29:03 -0800125 private int savedYPosition = -1;
126
Eric Erfanian938468d2017-10-24 14:05:52 -0700127 private final Runnable collapseRunnable =
128 new Runnable() {
129 @Override
130 public void run() {
131 textShowing = false;
132 if (hideAfterText) {
133 // Always reset here since text shouldn't keep showing.
134 hideAndReset();
135 } else {
yueg81a77ff2017-12-05 10:29:03 -0800136 viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_AVATAR_AND_ICON);
Eric Erfanian938468d2017-10-24 14:05:52 -0700137 }
138 }
139 };
140
Eric Erfanian938468d2017-10-24 14:05:52 -0700141 /** Type of action after bubble collapse */
142 @Retention(RetentionPolicy.SOURCE)
143 @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE})
144 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
145 public @interface CollapseEnd {
146 int NOTHING = 0;
147 int HIDE = 1;
148 }
149
150 @Retention(RetentionPolicy.SOURCE)
151 @IntDef({Visibility.ENTERING, Visibility.SHOWING, Visibility.EXITING, Visibility.HIDDEN})
152 private @interface Visibility {
153 int HIDDEN = 0;
154 int ENTERING = 1;
155 int SHOWING = 2;
156 int EXITING = 3;
157 }
158
159 /** Indicate bubble expansion state. */
160 @Retention(RetentionPolicy.SOURCE)
161 @IntDef({ExpansionState.START_EXPANDING, ExpansionState.START_COLLAPSING})
162 public @interface ExpansionState {
163 // TODO(yueg): add more states when needed
164 int START_EXPANDING = 0;
165 int START_COLLAPSING = 1;
166 }
167
168 /**
169 * Determines whether bubbles can be shown based on permissions obtained. This should be checked
170 * before attempting to create a Bubble.
171 *
172 * @return true iff bubbles are able to be shown.
173 * @see Settings#canDrawOverlays(Context)
174 */
175 public static boolean canShowBubbles(@NonNull Context context) {
176 return canShowBubblesForTesting != null
177 ? canShowBubblesForTesting
178 : Settings.canDrawOverlays(context);
179 }
180
181 @VisibleForTesting(otherwise = VisibleForTesting.NONE)
182 public static void setCanShowBubblesForTesting(boolean canShowBubbles) {
183 canShowBubblesForTesting = canShowBubbles;
184 }
185
186 /** Returns an Intent to request permission to show overlays */
187 @NonNull
188 public static Intent getRequestPermissionIntent(@NonNull Context context) {
189 return new Intent(
190 Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
191 Uri.fromParts("package", context.getPackageName(), null));
192 }
193
194 /** Creates instances of Bubble. The default implementation just calls the constructor. */
195 @VisibleForTesting
196 public interface BubbleFactory {
197 NewBubble createBubble(@NonNull Context context, @NonNull Handler handler);
198 }
199
200 private static BubbleFactory bubbleFactory = NewBubble::new;
201
202 public static NewBubble createBubble(@NonNull Context context, @NonNull NewBubbleInfo info) {
203 NewBubble bubble = bubbleFactory.createBubble(context, new Handler());
204 bubble.setBubbleInfo(info);
205 return bubble;
206 }
207
208 @VisibleForTesting
209 public static void setBubbleFactory(@NonNull BubbleFactory bubbleFactory) {
210 NewBubble.bubbleFactory = bubbleFactory;
211 }
212
213 @VisibleForTesting
214 public static void resetBubbleFactory() {
215 NewBubble.bubbleFactory = NewBubble::new;
216 }
217
218 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
219 NewBubble(@NonNull Context context, @NonNull Handler handler) {
220 context = new ContextThemeWrapper(context, R.style.Theme_AppCompat);
221 this.context = context;
222 this.handler = handler;
223 windowManager = context.getSystemService(WindowManager.class);
224
225 viewHolder = new ViewHolder(context);
yueg81a77ff2017-12-05 10:29:03 -0800226
227 leftBoundary =
228 context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal)
229 - context
230 .getResources()
231 .getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal);
yueg87111362017-12-08 12:45:50 -0800232 primaryIconMoveDistance =
233 context.getResources().getDimensionPixelSize(R.dimen.bubble_size)
234 - context.getResources().getDimensionPixelSize(R.dimen.bubble_small_icon_size);
Eric Erfanian938468d2017-10-24 14:05:52 -0700235 }
236
237 /** Expands the main bubble menu. */
yuegf539f782017-12-18 16:20:58 -0800238 public void expand() {
yuega235e132017-12-13 14:13:57 -0800239 setPrimaryButtonAccessibilityAction(
240 context.getString(R.string.a11y_bubble_primary_button_collapse_action));
yueg0b4755c2017-12-18 10:01:03 -0800241
yueg81a77ff2017-12-05 10:29:03 -0800242 viewHolder.setDrawerVisibility(View.INVISIBLE);
yueg0b4755c2017-12-18 10:01:03 -0800243 viewHolder.getArrow().setVisibility(View.INVISIBLE);
244 // No click during animation to avoid jank.
245 viewHolder.setPrimaryButtonClickable(false);
246
Eric Erfanian938468d2017-10-24 14:05:52 -0700247 View expandedView = viewHolder.getExpandedView();
248 expandedView
249 .getViewTreeObserver()
250 .addOnPreDrawListener(
251 new OnPreDrawListener() {
252 @Override
253 public boolean onPreDraw() {
yueg81a77ff2017-12-05 10:29:03 -0800254 // Move the whole bubble up so that expanded view is still in screen
255 int moveUpDistance = viewHolder.getMoveUpDistance();
256 if (moveUpDistance != 0) {
257 savedYPosition = windowParams.y;
258 }
259
yueg0b4755c2017-12-18 10:01:03 -0800260 // Animation 1: animate x-move and y-move (if needed) together
yueg81a77ff2017-12-05 10:29:03 -0800261 int deltaX =
262 (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
yueg0b4755c2017-12-18 10:01:03 -0800263 float k = -(float) moveUpDistance / deltaX;
yueg81a77ff2017-12-05 10:29:03 -0800264 if (isDrawingFromRight()) {
265 deltaX = -deltaX;
266 }
yueg0b4755c2017-12-18 10:01:03 -0800267 ValueAnimator xValueAnimator =
268 createBubbleMoveAnimator(
269 windowParams.x - deltaX, windowParams.x, windowParams.y, k);
yueg81a77ff2017-12-05 10:29:03 -0800270
yueg0b4755c2017-12-18 10:01:03 -0800271 // Show expanded view
272 expandedView.setVisibility(View.VISIBLE);
yueg81a77ff2017-12-05 10:29:03 -0800273
yueg0b4755c2017-12-18 10:01:03 -0800274 // Animator 2: reveal expanded view from top left or top right
275 View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
276 ValueAnimator revealAnim =
277 createOpenCloseOutlineProvider(expandedMenu)
278 .createRevealAnimator(expandedMenu, false);
279 revealAnim.setInterpolator(accelerateDecelerateInterpolator);
280
281 // Animator 3: expanded view fade in
282 Animator fadeIn = ObjectAnimator.ofFloat(expandedView, "alpha", 0, 1);
283 fadeIn.setInterpolator(accelerateDecelerateInterpolator);
284
285 // Play all animation together
286 AnimatorSet expandAnimatorSet = new AnimatorSet();
287 expandAnimatorSet.playTogether(revealAnim, fadeIn, xValueAnimator);
288 expandAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
289 expandAnimatorSet.addListener(
yueg87111362017-12-08 12:45:50 -0800290 new AnimatorListenerAdapter() {
yueg81a77ff2017-12-05 10:29:03 -0800291 @Override
292 public void onAnimationEnd(Animator animation) {
yueg0b4755c2017-12-18 10:01:03 -0800293 // Show arrow after animation
294 viewHolder.getArrow().setVisibility(View.VISIBLE);
295 // Safe to click primary button now
296 viewHolder.setPrimaryButtonClickable(true);
yueg81a77ff2017-12-05 10:29:03 -0800297 }
yueg81a77ff2017-12-05 10:29:03 -0800298 });
yueg0b4755c2017-12-18 10:01:03 -0800299 expandAnimatorSet.start();
yueg81a77ff2017-12-05 10:29:03 -0800300
Eric Erfanian938468d2017-10-24 14:05:52 -0700301 expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
Eric Erfanian938468d2017-10-24 14:05:52 -0700302 return false;
303 }
304 });
305 setFocused(true);
306 expanded = true;
307 }
308
yueg81a77ff2017-12-05 10:29:03 -0800309 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
yuegf539f782017-12-18 16:20:58 -0800310 public void startCollapse(@CollapseEnd int endAction, boolean shouldRecoverYPosition) {
yueg81a77ff2017-12-05 10:29:03 -0800311 View expandedView = viewHolder.getExpandedView();
312 if (expandedView.getVisibility() != View.VISIBLE || collapseAnimation != null) {
313 // Drawer is already collapsed or animation is running.
314 return;
315 }
316
317 overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT;
318 setFocused(false);
319
320 if (collapseEndAction == CollapseEnd.NOTHING) {
321 collapseEndAction = endAction;
322 }
yuega235e132017-12-13 14:13:57 -0800323 setPrimaryButtonAccessibilityAction(
324 context.getString(R.string.a11y_bubble_primary_button_expand_action));
yueg0b4755c2017-12-18 10:01:03 -0800325
326 // Hide arrow before animation
327 viewHolder.getArrow().setVisibility(View.INVISIBLE);
328
329 // No click during animation to avoid jank.
330 viewHolder.setPrimaryButtonClickable(false);
331
332 // Calculate animation values
333 int deltaX = (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
334 float k =
335 (savedYPosition != -1 && shouldRecoverYPosition)
336 ? (savedYPosition - windowParams.y) / (float) deltaX
337 : 0;
338 // The position is not useful after collapse
339 savedYPosition = -1;
340
341 // Animation 1: animate x-move and y-move (if needed) together
342 ValueAnimator xValueAnimator =
343 createBubbleMoveAnimator(windowParams.x, windowParams.x - deltaX, windowParams.y, k);
344
345 // Animator 2: hide expanded view to top left or top right
346 View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
347 ValueAnimator revealAnim =
348 createOpenCloseOutlineProvider(expandedMenu).createRevealAnimator(expandedMenu, true);
349 revealAnim.setInterpolator(accelerateDecelerateInterpolator);
350
351 // Animator 3: expanded view fade out
352 Animator fadeOut = ObjectAnimator.ofFloat(expandedView, "alpha", 1, 0);
353 fadeOut.setInterpolator(accelerateDecelerateInterpolator);
354
355 // Play all animation together
356 AnimatorSet collapseAnimatorSet = new AnimatorSet();
357 collapseAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
358 collapseAnimatorSet.playTogether(revealAnim, fadeOut, xValueAnimator);
359 collapseAnimatorSet.addListener(
360 new AnimatorListenerAdapter() {
361 @Override
362 public void onAnimationEnd(Animator animation) {
363 collapseAnimation = null;
364 expanded = false;
365
366 if (textShowing) {
367 // Will do resize once the text is done.
368 return;
369 }
370
371 // If this collapse was to come before a hide, do it now.
372 if (collapseEndAction == CollapseEnd.HIDE) {
373 hide();
374 }
375 collapseEndAction = CollapseEnd.NOTHING;
376
377 // If collapse on the right side, the primary button move left a bit after drawer
378 // visibility becoming GONE. To avoid it, we create a new ViewHolder.
379 // It also set primary button clickable back to true, so no need to reset manually.
380 replaceViewHolder();
381
382 // Resume normal gravity after any resizing is done.
383 handler.postDelayed(
yueg81a77ff2017-12-05 10:29:03 -0800384 () -> {
yueg0b4755c2017-12-18 10:01:03 -0800385 overrideGravity = null;
386 if (!viewHolder.isMoving()) {
387 viewHolder.undoGravityOverride();
yueg81a77ff2017-12-05 10:29:03 -0800388 }
yueg0b4755c2017-12-18 10:01:03 -0800389 },
390 // Need to wait twice as long for resize and layout
391 WINDOW_REDRAW_DELAY_MILLIS * 2);
392 }
393 });
394 collapseAnimatorSet.start();
yueg81a77ff2017-12-05 10:29:03 -0800395 }
396
Eric Erfanian938468d2017-10-24 14:05:52 -0700397 /**
398 * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is
399 * already showing this method does nothing.
400 */
401 public void show() {
402 if (collapseEndAction == CollapseEnd.HIDE) {
403 // If show() was called while collapsing, make sure we don't hide after.
404 collapseEndAction = CollapseEnd.NOTHING;
405 }
406 if (visibility == Visibility.SHOWING || visibility == Visibility.ENTERING) {
407 return;
408 }
409
410 hideAfterText = false;
411
yueg07e323c2017-12-19 16:05:47 -0800412 boolean isRtl =
413 TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
Eric Erfanian938468d2017-10-24 14:05:52 -0700414 if (windowParams == null) {
415 // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O.
416 @SuppressWarnings("deprecation")
417 @SuppressLint("InlinedApi")
418 int type =
419 BuildCompat.isAtLeastO()
420 ? LayoutParams.TYPE_APPLICATION_OVERLAY
421 : LayoutParams.TYPE_PHONE;
422
423 windowParams =
424 new LayoutParams(
425 type,
426 LayoutParams.FLAG_NOT_TOUCH_MODAL
427 | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
428 | LayoutParams.FLAG_NOT_FOCUSABLE
429 | LayoutParams.FLAG_LAYOUT_NO_LIMITS,
430 PixelFormat.TRANSLUCENT);
yueg07e323c2017-12-19 16:05:47 -0800431 windowParams.gravity = Gravity.TOP | (isRtl ? Gravity.RIGHT : Gravity.LEFT);
yueg81a77ff2017-12-05 10:29:03 -0800432 windowParams.x = leftBoundary;
Eric Erfanian938468d2017-10-24 14:05:52 -0700433 windowParams.y = currentInfo.getStartingYPosition();
434 windowParams.height = LayoutParams.WRAP_CONTENT;
435 windowParams.width = LayoutParams.WRAP_CONTENT;
436 }
437
yueg87111362017-12-08 12:45:50 -0800438 if (exitAnimatorSet != null) {
439 exitAnimatorSet.removeAllListeners();
440 exitAnimatorSet.cancel();
441 exitAnimatorSet = null;
Eric Erfanian938468d2017-10-24 14:05:52 -0700442 } else {
443 windowManager.addView(viewHolder.getRoot(), windowParams);
yueg87111362017-12-08 12:45:50 -0800444 viewHolder.getPrimaryButton().setVisibility(View.VISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700445 viewHolder.getPrimaryButton().setScaleX(0);
446 viewHolder.getPrimaryButton().setScaleY(0);
yueg87111362017-12-08 12:45:50 -0800447 viewHolder.getPrimaryAvatar().setAlpha(0f);
448 viewHolder.getPrimaryIcon().setAlpha(0f);
yueg07e323c2017-12-19 16:05:47 -0800449 if (isRtl) {
450 onLeftRightSwitch(true);
451 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700452 }
453
454 viewHolder.setChildClickable(true);
455 visibility = Visibility.ENTERING;
yueg87111362017-12-08 12:45:50 -0800456
yuega235e132017-12-13 14:13:57 -0800457 setPrimaryButtonAccessibilityAction(
458 context.getString(R.string.a11y_bubble_primary_button_expand_action));
459
yueg87111362017-12-08 12:45:50 -0800460 // Show bubble animation: scale the whole bubble to 1, and change avatar+icon's alpha to 1
461 ObjectAnimator scaleXAnimator =
462 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 1);
463 ObjectAnimator scaleYAnimator =
464 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 1);
465 ObjectAnimator avatarAlphaAnimator =
466 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 1);
467 ObjectAnimator iconAlphaAnimator =
468 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 1);
469 AnimatorSet enterAnimatorSet = new AnimatorSet();
470 enterAnimatorSet.playTogether(
471 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
472 enterAnimatorSet.setInterpolator(new OvershootInterpolator());
473 enterAnimatorSet.addListener(
474 new AnimatorListenerAdapter() {
475 @Override
476 public void onAnimationEnd(Animator animation) {
477 visibility = Visibility.SHOWING;
478 // Show the queued up text, if available.
479 if (textAfterShow != null) {
480 showText(textAfterShow);
481 textAfterShow = null;
482 }
483 }
484 });
485 enterAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700486
487 updatePrimaryIconAnimation();
488 }
489
490 /** Hide the bubble. */
491 public void hide() {
492 if (hideAfterText) {
493 // hideAndReset() will be called after showing text, do nothing here.
494 return;
495 }
496 hideHelper(this::defaultAfterHidingAnimation);
497 }
498
499 /** Hide the bubble and reset {@viewHolder} to initial state */
500 public void hideAndReset() {
501 hideHelper(
502 () -> {
503 defaultAfterHidingAnimation();
504 reset();
505 });
506 }
507
508 /** Returns whether the bubble is currently visible */
509 public boolean isVisible() {
510 return visibility == Visibility.SHOWING
511 || visibility == Visibility.ENTERING
512 || visibility == Visibility.EXITING;
513 }
514
515 /**
516 * Set the info for this Bubble to display
517 *
518 * @param bubbleInfo the BubbleInfo to display in this Bubble.
519 */
520 public void setBubbleInfo(@NonNull NewBubbleInfo bubbleInfo) {
521 currentInfo = bubbleInfo;
522 update();
523 }
524
525 /**
526 * Update the state and behavior of actions.
527 *
528 * @param actions the new state of the bubble's actions
529 */
530 public void updateActions(@NonNull List<Action> actions) {
531 currentInfo = NewBubbleInfo.from(currentInfo).setActions(actions).build();
532 updateButtonStates();
533 }
534
yuega5a08d82017-10-31 14:11:53 -0700535 /**
536 * Update the avatar from photo.
537 *
538 * @param avatar the new photo avatar in the bubble's primary button
539 */
540 public void updatePhotoAvatar(@NonNull Drawable avatar) {
541 // Make it round
542 int bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
543 Drawable roundAvatar =
544 DrawableConverter.getRoundedDrawable(context, avatar, bubbleSize, bubbleSize);
545
546 updateAvatar(roundAvatar);
547 }
548
549 /**
550 * Update the avatar.
551 *
552 * @param avatar the new avatar in the bubble's primary button
553 */
554 public void updateAvatar(@NonNull Drawable avatar) {
555 if (!avatar.equals(currentInfo.getAvatar())) {
556 currentInfo = NewBubbleInfo.from(currentInfo).setAvatar(avatar).build();
557 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
558 }
559 }
560
Eric Erfanian938468d2017-10-24 14:05:52 -0700561 /** Returns the currently displayed NewBubbleInfo */
562 public NewBubbleInfo getBubbleInfo() {
563 return currentInfo;
564 }
565
566 /**
567 * Display text in the main bubble. The bubble's drawer is not expandable while text is showing,
568 * and the drawer will be closed if already open.
569 *
570 * @param text the text to display to the user
571 */
572 public void showText(@NonNull CharSequence text) {
573 textShowing = true;
574 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800575 startCollapse(CollapseEnd.NOTHING, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700576 doShowText(text);
577 } else {
578 // Need to transition from old bounds to new bounds manually
579 NewChangeOnScreenBounds transition = new NewChangeOnScreenBounds();
580 // Prepare and capture start values
581 TransitionValues startValues = new TransitionValues();
582 startValues.view = viewHolder.getPrimaryButton();
583 transition.addTarget(startValues.view);
584 transition.captureStartValues(startValues);
585
586 // If our view is not laid out yet, postpone showing the text.
587 if (startValues.values.isEmpty()) {
588 textAfterShow = text;
589 return;
590 }
591
yueg81a77ff2017-12-05 10:29:03 -0800592 doShowText(text);
593 // Hide the text so we can animate it in
594 viewHolder.getPrimaryText().setAlpha(0);
Eric Erfanian938468d2017-10-24 14:05:52 -0700595
yueg81a77ff2017-12-05 10:29:03 -0800596 ViewAnimator primaryButton = viewHolder.getPrimaryButton();
597 // Cancel the automatic transition scheduled in doShowText
598 TransitionManager.endTransitions((ViewGroup) primaryButton.getParent());
599 primaryButton
600 .getViewTreeObserver()
601 .addOnPreDrawListener(
602 new OnPreDrawListener() {
603 @Override
604 public boolean onPreDraw() {
605 primaryButton.getViewTreeObserver().removeOnPreDrawListener(this);
Eric Erfanian938468d2017-10-24 14:05:52 -0700606
yueg81a77ff2017-12-05 10:29:03 -0800607 // Prepare and capture end values, always use the size of primaryText since
608 // its invisibility makes primaryButton smaller than expected
609 TransitionValues endValues = new TransitionValues();
610 endValues.values.put(
611 NewChangeOnScreenBounds.PROPNAME_WIDTH,
612 viewHolder.getPrimaryText().getWidth());
613 endValues.values.put(
614 NewChangeOnScreenBounds.PROPNAME_HEIGHT,
615 viewHolder.getPrimaryText().getHeight());
616 endValues.view = primaryButton;
617 transition.addTarget(endValues.view);
618 transition.captureEndValues(endValues);
Eric Erfanian938468d2017-10-24 14:05:52 -0700619
yueg81a77ff2017-12-05 10:29:03 -0800620 // animate the primary button bounds change
621 Animator bounds =
622 transition.createAnimator(primaryButton, startValues, endValues);
Eric Erfanian938468d2017-10-24 14:05:52 -0700623
yueg81a77ff2017-12-05 10:29:03 -0800624 // Animate the text in
625 Animator alpha =
626 ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f);
Eric Erfanian938468d2017-10-24 14:05:52 -0700627
yueg81a77ff2017-12-05 10:29:03 -0800628 AnimatorSet set = new AnimatorSet();
629 set.play(bounds).before(alpha);
630 set.start();
631 return false;
632 }
633 });
Eric Erfanian938468d2017-10-24 14:05:52 -0700634 }
635 handler.removeCallbacks(collapseRunnable);
636 handler.postDelayed(collapseRunnable, SHOW_TEXT_DURATION_MILLIS);
637 }
638
Eric Erfanian938468d2017-10-24 14:05:52 -0700639 @Nullable
640 Integer getGravityOverride() {
641 return overrideGravity;
642 }
643
644 void onMoveStart() {
yueg81a77ff2017-12-05 10:29:03 -0800645 if (viewHolder.getExpandedView().getVisibility() == View.VISIBLE) {
646 viewHolder.setDrawerVisibility(View.INVISIBLE);
647 }
648 expanded = false;
649 savedYPosition = -1;
650
Eric Erfanian938468d2017-10-24 14:05:52 -0700651 viewHolder
652 .getPrimaryButton()
653 .animate()
654 .translationZ(
yuegc6deafc2017-11-06 16:42:13 -0800655 context
656 .getResources()
657 .getDimensionPixelOffset(R.dimen.bubble_dragging_elevation_change));
Eric Erfanian938468d2017-10-24 14:05:52 -0700658 }
659
660 void onMoveFinish() {
661 viewHolder.getPrimaryButton().animate().translationZ(0);
Eric Erfanian938468d2017-10-24 14:05:52 -0700662 }
663
664 void primaryButtonClick() {
665 if (textShowing || currentInfo.getActions().isEmpty()) {
666 return;
667 }
668 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800669 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
670 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700671 } else {
yuegf539f782017-12-18 16:20:58 -0800672 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_EXPAND);
673 expand();
Eric Erfanian938468d2017-10-24 14:05:52 -0700674 }
675 }
676
yueg81a77ff2017-12-05 10:29:03 -0800677 void onLeftRightSwitch(boolean onRight) {
yueg87111362017-12-08 12:45:50 -0800678 // Move primary icon to the other side so it's not partially hiden
yueg81a77ff2017-12-05 10:29:03 -0800679 View primaryIcon = viewHolder.getPrimaryIcon();
yueg87111362017-12-08 12:45:50 -0800680 primaryIcon.animate().translationX(onRight ? -primaryIconMoveDistance : 0).start();
yueg81a77ff2017-12-05 10:29:03 -0800681 }
682
Eric Erfanian938468d2017-10-24 14:05:52 -0700683 LayoutParams getWindowParams() {
684 return windowParams;
685 }
686
687 View getRootView() {
688 return viewHolder.getRoot();
689 }
690
691 /**
692 * Hide the bubble if visible. Will run a short exit animation and before hiding, and {@code
693 * afterHiding} after hiding. If the bubble is currently showing text, will hide after the text is
694 * done displaying. If the bubble is not visible this method does nothing.
695 */
yueg87111362017-12-08 12:45:50 -0800696 @VisibleForTesting
697 void hideHelper(Runnable afterHiding) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700698 if (visibility == Visibility.HIDDEN || visibility == Visibility.EXITING) {
699 return;
700 }
701
702 // Make bubble non clickable to prevent further buggy actions
703 viewHolder.setChildClickable(false);
704
705 if (textShowing) {
706 hideAfterText = true;
707 return;
708 }
709
710 if (collapseAnimation != null) {
711 collapseEndAction = CollapseEnd.HIDE;
712 return;
713 }
714
715 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800716 startCollapse(CollapseEnd.HIDE, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700717 return;
718 }
719
720 visibility = Visibility.EXITING;
yueg87111362017-12-08 12:45:50 -0800721
722 // Hide bubble animation: scale the whole bubble to 0, and change avatar+icon's alpha to 0
723 ObjectAnimator scaleXAnimator =
724 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 0);
725 ObjectAnimator scaleYAnimator =
726 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 0);
727 ObjectAnimator avatarAlphaAnimator =
728 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 0);
729 ObjectAnimator iconAlphaAnimator =
730 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 0);
731 exitAnimatorSet = new AnimatorSet();
732 exitAnimatorSet.playTogether(
733 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
734 exitAnimatorSet.setInterpolator(new AnticipateInterpolator());
735 exitAnimatorSet.addListener(
736 new AnimatorListenerAdapter() {
737 @Override
yuega235e132017-12-13 14:13:57 -0800738 public void onAnimationStart(Animator animation) {
739 viewHolder.getPrimaryButton().setAccessibilityDelegate(null);
740 }
741
742 @Override
yueg87111362017-12-08 12:45:50 -0800743 public void onAnimationEnd(Animator animation) {
744 afterHiding.run();
745 }
746 });
747 exitAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700748 }
749
750 private void reset() {
751 viewHolder = new ViewHolder(viewHolder.getRoot().getContext());
752 update();
753 }
754
755 private void update() {
yuega5a08d82017-10-31 14:11:53 -0700756 // Whole primary button background
yueg84ac49b2017-11-01 16:22:28 -0700757 Drawable backgroundCirle =
758 context.getResources().getDrawable(R.drawable.bubble_shape_circle, context.getTheme());
Eric Erfanian938468d2017-10-24 14:05:52 -0700759 int primaryTint =
760 ColorUtils.compositeColors(
761 context.getColor(R.color.bubble_primary_background_darken),
762 currentInfo.getPrimaryColor());
yueg84ac49b2017-11-01 16:22:28 -0700763 backgroundCirle.mutate().setTint(primaryTint);
764 viewHolder.getPrimaryButton().setBackground(backgroundCirle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700765
yuega5a08d82017-10-31 14:11:53 -0700766 // Small icon
yueg84ac49b2017-11-01 16:22:28 -0700767 Drawable smallIconBackgroundCircle =
768 context
769 .getResources()
770 .getDrawable(R.drawable.bubble_shape_circle_small, context.getTheme());
771 smallIconBackgroundCircle.setTint(context.getColor(R.color.bubble_button_color_blue));
772 viewHolder.getPrimaryIcon().setBackground(smallIconBackgroundCircle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700773 viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon());
yuega5a08d82017-10-31 14:11:53 -0700774 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
Eric Erfanian938468d2017-10-24 14:05:52 -0700775
yuega5a08d82017-10-31 14:11:53 -0700776 updatePrimaryIconAnimation();
Eric Erfanian938468d2017-10-24 14:05:52 -0700777 updateButtonStates();
778 }
779
780 private void updatePrimaryIconAnimation() {
781 Drawable drawable = viewHolder.getPrimaryIcon().getDrawable();
782 if (drawable instanceof Animatable) {
783 if (isVisible()) {
784 ((Animatable) drawable).start();
785 } else {
786 ((Animatable) drawable).stop();
787 }
788 }
789 }
790
791 private void updateButtonStates() {
yueg84ac49b2017-11-01 16:22:28 -0700792 configureButton(currentInfo.getActions().get(0), viewHolder.getFullScreenButton());
793 configureButton(currentInfo.getActions().get(1), viewHolder.getMuteButton());
794 configureButton(currentInfo.getActions().get(2), viewHolder.getAudioRouteButton());
795 configureButton(currentInfo.getActions().get(3), viewHolder.getEndCallButton());
Eric Erfanian938468d2017-10-24 14:05:52 -0700796 }
797
yueg87111362017-12-08 12:45:50 -0800798 @VisibleForTesting
799 void doShowText(@NonNull CharSequence text) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700800 TransitionManager.beginDelayedTransition((ViewGroup) viewHolder.getPrimaryButton().getParent());
801 viewHolder.getPrimaryText().setText(text);
802 viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_TEXT);
803 }
804
yueg84ac49b2017-11-01 16:22:28 -0700805 private void configureButton(Action action, NewCheckableButton button) {
yueg07e323c2017-12-19 16:05:47 -0800806 boolean isRtl =
807 TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
808 if (isRtl) {
809 button.setCompoundDrawablesWithIntrinsicBounds(null, null, action.getIconDrawable(), null);
810 } else {
811 button.setCompoundDrawablesWithIntrinsicBounds(action.getIconDrawable(), null, null, null);
812 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700813 button.setChecked(action.isChecked());
814 button.setEnabled(action.isEnabled());
yueg84ac49b2017-11-01 16:22:28 -0700815 button.setText(action.getName());
yuega235e132017-12-13 14:13:57 -0800816 button.setContentDescription(action.getName());
Eric Erfanian938468d2017-10-24 14:05:52 -0700817 button.setOnClickListener(v -> doAction(action));
818 }
819
820 private void doAction(Action action) {
821 try {
822 action.getIntent().send();
823 } catch (CanceledException e) {
824 throw new RuntimeException(e);
825 }
826 }
827
yueg81a77ff2017-12-05 10:29:03 -0800828 /**
829 * Create a new ViewHolder object to replace the old one.It only happens when not moving and
830 * collapsed.
831 */
832 void replaceViewHolder() {
833 LogUtil.enterBlock("NewBubble.replaceViewHolder");
yuegf473e1d2018-01-02 16:23:14 -0800834 // Don't do it. If windowParams is null, either we haven't initialized it or we set it to null.
835 // There is no need to recreate bubble.
836 if (windowParams == null) {
837 return;
838 }
839
Eric Erfanian938468d2017-10-24 14:05:52 -0700840 ViewHolder oldViewHolder = viewHolder;
Eric Erfanian938468d2017-10-24 14:05:52 -0700841
yueg81a77ff2017-12-05 10:29:03 -0800842 // Create a new ViewHolder and copy needed info.
843 viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext());
844 viewHolder
845 .getPrimaryButton()
846 .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild());
847 viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText());
yueg87111362017-12-08 12:45:50 -0800848 viewHolder.getPrimaryIcon().setX(isDrawingFromRight() ? 0 : primaryIconMoveDistance);
yueg81a77ff2017-12-05 10:29:03 -0800849 viewHolder
850 .getPrimaryIcon()
yueg87111362017-12-08 12:45:50 -0800851 .setTranslationX(isDrawingFromRight() ? -primaryIconMoveDistance : 0);
yuega235e132017-12-13 14:13:57 -0800852 setPrimaryButtonAccessibilityAction(
853 context.getString(R.string.a11y_bubble_primary_button_expand_action));
Eric Erfanian938468d2017-10-24 14:05:52 -0700854
yueg81a77ff2017-12-05 10:29:03 -0800855 update();
856
857 // Add new view at its horizontal boundary
Eric Erfanian938468d2017-10-24 14:05:52 -0700858 ViewGroup root = viewHolder.getRoot();
yueg81a77ff2017-12-05 10:29:03 -0800859 windowParams.x = leftBoundary;
860 windowParams.gravity = Gravity.TOP | (isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT);
Eric Erfanian938468d2017-10-24 14:05:52 -0700861 windowManager.addView(root, windowParams);
yueg81a77ff2017-12-05 10:29:03 -0800862
863 // Remove the old view after delay
Eric Erfanian938468d2017-10-24 14:05:52 -0700864 root.getViewTreeObserver()
865 .addOnPreDrawListener(
866 new OnPreDrawListener() {
867 @Override
868 public boolean onPreDraw() {
869 root.getViewTreeObserver().removeOnPreDrawListener(this);
870 // Wait a bit before removing the old view; make sure the new one has drawn over it.
871 handler.postDelayed(
872 () -> windowManager.removeView(oldViewHolder.getRoot()),
873 WINDOW_REDRAW_DELAY_MILLIS);
874 return true;
875 }
876 });
877 }
878
yueg81a77ff2017-12-05 10:29:03 -0800879 int getDrawerVisibility() {
880 return viewHolder.getExpandedView().getVisibility();
Eric Erfanian938468d2017-10-24 14:05:52 -0700881 }
882
yuegf473e1d2018-01-02 16:23:14 -0800883 void bottomActionDismiss() {
884 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_DISMISS);
885 // Create bubble at default location at next time
886 hideAndReset();
887 windowParams = null;
888 }
889
890 void bottomActionEndCall() {
891 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_END_CALL);
892 // Hide without animation
893 hideHelper(
894 () -> {
895 defaultAfterHidingAnimation();
896 DialerCall call = getCall();
897 if (call != null) {
898 call.disconnect();
899 Toast.makeText(context, R.string.incall_call_ended, Toast.LENGTH_SHORT).show();
900 }
901 });
902 }
903
Eric Erfanian938468d2017-10-24 14:05:52 -0700904 private boolean isDrawingFromRight() {
905 return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
906 }
907
908 private void setFocused(boolean focused) {
909 if (focused) {
910 windowParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
911 } else {
912 windowParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
913 }
914 windowManager.updateViewLayout(getRootView(), windowParams);
915 }
916
917 private void defaultAfterHidingAnimation() {
yueg87111362017-12-08 12:45:50 -0800918 exitAnimatorSet = null;
919 viewHolder.getPrimaryButton().setVisibility(View.INVISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700920 windowManager.removeView(viewHolder.getRoot());
921 visibility = Visibility.HIDDEN;
922
923 updatePrimaryIconAnimation();
924 }
925
yueg81a77ff2017-12-05 10:29:03 -0800926 private void logBasicOrCallImpression(DialerImpression.Type impressionType) {
yuegf473e1d2018-01-02 16:23:14 -0800927 DialerCall call = getCall();
yueg81a77ff2017-12-05 10:29:03 -0800928 if (call != null) {
929 Logger.get(context)
930 .logCallImpression(impressionType, call.getUniqueCallId(), call.getTimeAddedMs());
931 } else {
932 Logger.get(context).logImpression(impressionType);
933 }
934 }
935
yuegf473e1d2018-01-02 16:23:14 -0800936 private DialerCall getCall() {
937 // Bubble is shown for outgoing, active or background call
938 DialerCall call = CallList.getInstance().getOutgoingCall();
939 if (call == null) {
940 call = CallList.getInstance().getActiveOrBackgroundCall();
941 }
942 return call;
943 }
944
yuega235e132017-12-13 14:13:57 -0800945 private void setPrimaryButtonAccessibilityAction(String description) {
946 viewHolder
947 .getPrimaryButton()
948 .setAccessibilityDelegate(
949 new AccessibilityDelegate() {
950 @Override
951 public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) {
952 super.onInitializeAccessibilityNodeInfo(v, info);
953
954 AccessibilityAction clickAction =
955 new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, description);
956 info.addAction(clickAction);
957 }
958 });
959 }
960
yueg0b4755c2017-12-18 10:01:03 -0800961 private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider(View view) {
962 int startRectX = isDrawingFromRight() ? view.getMeasuredWidth() : 0;
963 Rect startRect = new Rect(startRectX, 0, startRectX, 0);
964 Rect endRect = new Rect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
965
966 float bubbleRadius = context.getResources().getDimension(R.dimen.bubble_radius);
967 return new RoundedRectRevealOutlineProvider(bubbleRadius, bubbleRadius, startRect, endRect);
968 }
969
970 private ValueAnimator createBubbleMoveAnimator(int startX, int endX, int startY, float k) {
971 ValueAnimator xValueAnimator = ValueAnimator.ofFloat(startX, endX);
972 xValueAnimator.setInterpolator(new LinearOutSlowInInterpolator());
973 xValueAnimator.addUpdateListener(
974 (valueAnimator) -> {
975 // Update windowParams and the root layout.
976 // We can't do ViewPropertyAnimation since it clips children.
977 float newX = (float) valueAnimator.getAnimatedValue();
978 if (k != 0) {
979 windowParams.y = startY + (int) (Math.abs(newX - (float) startX) * k);
980 }
981 windowParams.x = (int) newX;
982 windowManager.updateViewLayout(viewHolder.getRoot(), windowParams);
983 });
984 return xValueAnimator;
985 }
986
Eric Erfanian938468d2017-10-24 14:05:52 -0700987 @VisibleForTesting
988 class ViewHolder {
989
yuega5a08d82017-10-31 14:11:53 -0700990 public static final int CHILD_INDEX_AVATAR_AND_ICON = 0;
Eric Erfanian938468d2017-10-24 14:05:52 -0700991 public static final int CHILD_INDEX_TEXT = 1;
992
yueg0b4755c2017-12-18 10:01:03 -0800993 private NewMoveHandler moveHandler;
Eric Erfanian938468d2017-10-24 14:05:52 -0700994 private final NewWindowRoot root;
995 private final ViewAnimator primaryButton;
996 private final ImageView primaryIcon;
yuega5a08d82017-10-31 14:11:53 -0700997 private final ImageView primaryAvatar;
Eric Erfanian938468d2017-10-24 14:05:52 -0700998 private final TextView primaryText;
yueg0b4755c2017-12-18 10:01:03 -0800999 private final View arrow;
Eric Erfanian938468d2017-10-24 14:05:52 -07001000
1001 private final NewCheckableButton fullScreenButton;
1002 private final NewCheckableButton muteButton;
1003 private final NewCheckableButton audioRouteButton;
1004 private final NewCheckableButton endCallButton;
1005 private final View expandedView;
1006
1007 public ViewHolder(Context context) {
1008 // Window root is not in the layout file so that the inflater has a view to inflate into
1009 this.root = new NewWindowRoot(context);
1010 LayoutInflater inflater = LayoutInflater.from(root.getContext());
1011 View contentView = inflater.inflate(R.layout.new_bubble_base, root, true);
1012 expandedView = contentView.findViewById(R.id.bubble_expanded_layout);
1013 primaryButton = contentView.findViewById(R.id.bubble_button_primary);
yuega5a08d82017-10-31 14:11:53 -07001014 primaryAvatar = contentView.findViewById(R.id.bubble_icon_avatar);
Eric Erfanian938468d2017-10-24 14:05:52 -07001015 primaryIcon = contentView.findViewById(R.id.bubble_icon_primary);
1016 primaryText = contentView.findViewById(R.id.bubble_text);
yueg0b4755c2017-12-18 10:01:03 -08001017 arrow = contentView.findViewById(R.id.bubble_triangle);
Eric Erfanian938468d2017-10-24 14:05:52 -07001018
1019 fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen);
1020 muteButton = contentView.findViewById(R.id.bubble_button_mute);
1021 audioRouteButton = contentView.findViewById(R.id.bubble_button_audio_route);
1022 endCallButton = contentView.findViewById(R.id.bubble_button_end_call);
1023
1024 root.setOnBackPressedListener(
1025 () -> {
1026 if (visibility == Visibility.SHOWING && expanded) {
yuegf539f782017-12-18 16:20:58 -08001027 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
1028 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -07001029 return true;
1030 }
1031 return false;
1032 });
1033 root.setOnConfigurationChangedListener(
1034 (configuration) -> {
1035 // The values in the current MoveHandler may be stale, so replace it. Then ensure the
1036 // Window is in bounds
1037 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
1038 moveHandler.snapToBounds();
1039 });
1040 root.setOnTouchListener(
1041 (v, event) -> {
1042 if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
yuegf539f782017-12-18 16:20:58 -08001043 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
1044 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -07001045 return true;
1046 }
1047 return false;
1048 });
1049 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
1050 }
1051
1052 private void setChildClickable(boolean clickable) {
1053 fullScreenButton.setClickable(clickable);
1054 muteButton.setClickable(clickable);
1055 audioRouteButton.setClickable(clickable);
1056 endCallButton.setClickable(clickable);
yueg0b4755c2017-12-18 10:01:03 -08001057 setPrimaryButtonClickable(clickable);
1058 }
Eric Erfanian938468d2017-10-24 14:05:52 -07001059
yueg0b4755c2017-12-18 10:01:03 -08001060 private void setPrimaryButtonClickable(boolean clickable) {
Eric Erfanian938468d2017-10-24 14:05:52 -07001061 moveHandler.setClickable(clickable);
1062 }
1063
yueg81a77ff2017-12-05 10:29:03 -08001064 public int getMoveUpDistance() {
1065 int deltaAllowed =
1066 expandedView.getHeight()
1067 - context
1068 .getResources()
1069 .getDimensionPixelOffset(R.dimen.bubble_button_padding_vertical)
1070 * 2;
1071 return moveHandler.getMoveUpDistance(deltaAllowed);
1072 }
1073
Eric Erfanian938468d2017-10-24 14:05:52 -07001074 public ViewGroup getRoot() {
1075 return root;
1076 }
1077
1078 public ViewAnimator getPrimaryButton() {
1079 return primaryButton;
1080 }
1081
1082 public ImageView getPrimaryIcon() {
1083 return primaryIcon;
1084 }
1085
yuega5a08d82017-10-31 14:11:53 -07001086 public ImageView getPrimaryAvatar() {
1087 return primaryAvatar;
1088 }
1089
Eric Erfanian938468d2017-10-24 14:05:52 -07001090 public TextView getPrimaryText() {
1091 return primaryText;
1092 }
1093
1094 public View getExpandedView() {
1095 return expandedView;
1096 }
1097
yueg0b4755c2017-12-18 10:01:03 -08001098 public View getArrow() {
1099 return arrow;
1100 }
1101
Eric Erfanian938468d2017-10-24 14:05:52 -07001102 public NewCheckableButton getFullScreenButton() {
1103 return fullScreenButton;
1104 }
1105
1106 public NewCheckableButton getMuteButton() {
1107 return muteButton;
1108 }
1109
1110 public NewCheckableButton getAudioRouteButton() {
1111 return audioRouteButton;
1112 }
1113
1114 public NewCheckableButton getEndCallButton() {
1115 return endCallButton;
1116 }
1117
1118 public void setDrawerVisibility(int visibility) {
1119 expandedView.setVisibility(visibility);
1120 }
1121
1122 public boolean isMoving() {
1123 return moveHandler.isMoving();
1124 }
1125
1126 public void undoGravityOverride() {
1127 moveHandler.undoGravityOverride();
1128 }
1129 }
Eric Erfanian938468d2017-10-24 14:05:52 -07001130}