blob: 469c15d71a051bc937a71d71ac190e28fe3a1afa [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;
63import android.widget.ViewAnimator;
yueg81a77ff2017-12-05 10:29:03 -080064import com.android.dialer.common.LogUtil;
65import com.android.dialer.logging.DialerImpression;
66import com.android.dialer.logging.Logger;
yuega5a08d82017-10-31 14:11:53 -070067import com.android.dialer.util.DrawableConverter;
yueg81a77ff2017-12-05 10:29:03 -080068import com.android.incallui.call.CallList;
69import com.android.incallui.call.DialerCall;
Eric Erfanian938468d2017-10-24 14:05:52 -070070import com.android.newbubble.NewBubbleInfo.Action;
71import java.lang.annotation.Retention;
72import java.lang.annotation.RetentionPolicy;
73import java.util.List;
yueg07e323c2017-12-19 16:05:47 -080074import java.util.Locale;
Eric Erfanian938468d2017-10-24 14:05:52 -070075
76/**
77 * Creates and manages a bubble window from information in a {@link NewBubbleInfo}. Before creating,
78 * be sure to check whether bubbles may be shown using {@link #canShowBubbles(Context)} and request
79 * permission if necessary ({@link #getRequestPermissionIntent(Context)} is provided for
80 * convenience)
81 */
82public class NewBubble {
83 // This class has some odd behavior that is not immediately obvious in order to avoid jank when
84 // resizing. See http://go/bubble-resize for details.
85
86 // How long text should show after showText(CharSequence) is called
87 private static final int SHOW_TEXT_DURATION_MILLIS = 3000;
88 // How long the new window should show before destroying the old one during resize operations.
89 // This ensures the new window has had time to draw first.
90 private static final int WINDOW_REDRAW_DELAY_MILLIS = 50;
91
yueg0b4755c2017-12-18 10:01:03 -080092 private static final int EXPAND_AND_COLLAPSE_ANIMATION_DURATION = 200;
93
Eric Erfanian938468d2017-10-24 14:05:52 -070094 private static Boolean canShowBubblesForTesting = null;
95
yueg0b4755c2017-12-18 10:01:03 -080096 private final AccelerateDecelerateInterpolator accelerateDecelerateInterpolator =
97 new AccelerateDecelerateInterpolator();
98
Eric Erfanian938468d2017-10-24 14:05:52 -070099 private final Context context;
100 private final WindowManager windowManager;
101
102 private final Handler handler;
103 private LayoutParams windowParams;
104
105 // Initialized in factory method
106 @SuppressWarnings("NullableProblems")
107 @NonNull
108 private NewBubbleInfo currentInfo;
109
110 @Visibility private int visibility;
111 private boolean expanded;
112 private boolean textShowing;
113 private boolean hideAfterText;
114 private CharSequence textAfterShow;
115 private int collapseEndAction;
116
yueg81a77ff2017-12-05 10:29:03 -0800117 ViewHolder viewHolder;
Eric Erfanian938468d2017-10-24 14:05:52 -0700118 private ViewPropertyAnimator collapseAnimation;
119 private Integer overrideGravity;
yueg87111362017-12-08 12:45:50 -0800120 @VisibleForTesting AnimatorSet exitAnimatorSet;
Eric Erfanian938468d2017-10-24 14:05:52 -0700121
yueg87111362017-12-08 12:45:50 -0800122 private final int primaryIconMoveDistance;
123 private final int leftBoundary;
yueg81a77ff2017-12-05 10:29:03 -0800124 private int savedYPosition = -1;
125
Eric Erfanian938468d2017-10-24 14:05:52 -0700126 private final Runnable collapseRunnable =
127 new Runnable() {
128 @Override
129 public void run() {
130 textShowing = false;
131 if (hideAfterText) {
132 // Always reset here since text shouldn't keep showing.
133 hideAndReset();
134 } else {
yueg81a77ff2017-12-05 10:29:03 -0800135 viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_AVATAR_AND_ICON);
Eric Erfanian938468d2017-10-24 14:05:52 -0700136 }
137 }
138 };
139
Eric Erfanian938468d2017-10-24 14:05:52 -0700140 /** Type of action after bubble collapse */
141 @Retention(RetentionPolicy.SOURCE)
142 @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE})
143 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
144 public @interface CollapseEnd {
145 int NOTHING = 0;
146 int HIDE = 1;
147 }
148
149 @Retention(RetentionPolicy.SOURCE)
150 @IntDef({Visibility.ENTERING, Visibility.SHOWING, Visibility.EXITING, Visibility.HIDDEN})
151 private @interface Visibility {
152 int HIDDEN = 0;
153 int ENTERING = 1;
154 int SHOWING = 2;
155 int EXITING = 3;
156 }
157
158 /** Indicate bubble expansion state. */
159 @Retention(RetentionPolicy.SOURCE)
160 @IntDef({ExpansionState.START_EXPANDING, ExpansionState.START_COLLAPSING})
161 public @interface ExpansionState {
162 // TODO(yueg): add more states when needed
163 int START_EXPANDING = 0;
164 int START_COLLAPSING = 1;
165 }
166
167 /**
168 * Determines whether bubbles can be shown based on permissions obtained. This should be checked
169 * before attempting to create a Bubble.
170 *
171 * @return true iff bubbles are able to be shown.
172 * @see Settings#canDrawOverlays(Context)
173 */
174 public static boolean canShowBubbles(@NonNull Context context) {
175 return canShowBubblesForTesting != null
176 ? canShowBubblesForTesting
177 : Settings.canDrawOverlays(context);
178 }
179
180 @VisibleForTesting(otherwise = VisibleForTesting.NONE)
181 public static void setCanShowBubblesForTesting(boolean canShowBubbles) {
182 canShowBubblesForTesting = canShowBubbles;
183 }
184
185 /** Returns an Intent to request permission to show overlays */
186 @NonNull
187 public static Intent getRequestPermissionIntent(@NonNull Context context) {
188 return new Intent(
189 Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
190 Uri.fromParts("package", context.getPackageName(), null));
191 }
192
193 /** Creates instances of Bubble. The default implementation just calls the constructor. */
194 @VisibleForTesting
195 public interface BubbleFactory {
196 NewBubble createBubble(@NonNull Context context, @NonNull Handler handler);
197 }
198
199 private static BubbleFactory bubbleFactory = NewBubble::new;
200
201 public static NewBubble createBubble(@NonNull Context context, @NonNull NewBubbleInfo info) {
202 NewBubble bubble = bubbleFactory.createBubble(context, new Handler());
203 bubble.setBubbleInfo(info);
204 return bubble;
205 }
206
207 @VisibleForTesting
208 public static void setBubbleFactory(@NonNull BubbleFactory bubbleFactory) {
209 NewBubble.bubbleFactory = bubbleFactory;
210 }
211
212 @VisibleForTesting
213 public static void resetBubbleFactory() {
214 NewBubble.bubbleFactory = NewBubble::new;
215 }
216
217 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
218 NewBubble(@NonNull Context context, @NonNull Handler handler) {
219 context = new ContextThemeWrapper(context, R.style.Theme_AppCompat);
220 this.context = context;
221 this.handler = handler;
222 windowManager = context.getSystemService(WindowManager.class);
223
224 viewHolder = new ViewHolder(context);
yueg81a77ff2017-12-05 10:29:03 -0800225
226 leftBoundary =
227 context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal)
228 - context
229 .getResources()
230 .getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal);
yueg87111362017-12-08 12:45:50 -0800231 primaryIconMoveDistance =
232 context.getResources().getDimensionPixelSize(R.dimen.bubble_size)
233 - context.getResources().getDimensionPixelSize(R.dimen.bubble_small_icon_size);
Eric Erfanian938468d2017-10-24 14:05:52 -0700234 }
235
236 /** Expands the main bubble menu. */
yuegf539f782017-12-18 16:20:58 -0800237 public void expand() {
yuega235e132017-12-13 14:13:57 -0800238 setPrimaryButtonAccessibilityAction(
239 context.getString(R.string.a11y_bubble_primary_button_collapse_action));
yueg0b4755c2017-12-18 10:01:03 -0800240
yueg81a77ff2017-12-05 10:29:03 -0800241 viewHolder.setDrawerVisibility(View.INVISIBLE);
yueg0b4755c2017-12-18 10:01:03 -0800242 viewHolder.getArrow().setVisibility(View.INVISIBLE);
243 // No click during animation to avoid jank.
244 viewHolder.setPrimaryButtonClickable(false);
245
Eric Erfanian938468d2017-10-24 14:05:52 -0700246 View expandedView = viewHolder.getExpandedView();
247 expandedView
248 .getViewTreeObserver()
249 .addOnPreDrawListener(
250 new OnPreDrawListener() {
251 @Override
252 public boolean onPreDraw() {
yueg81a77ff2017-12-05 10:29:03 -0800253 // Move the whole bubble up so that expanded view is still in screen
254 int moveUpDistance = viewHolder.getMoveUpDistance();
255 if (moveUpDistance != 0) {
256 savedYPosition = windowParams.y;
257 }
258
yueg0b4755c2017-12-18 10:01:03 -0800259 // Animation 1: animate x-move and y-move (if needed) together
yueg81a77ff2017-12-05 10:29:03 -0800260 int deltaX =
261 (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
yueg0b4755c2017-12-18 10:01:03 -0800262 float k = -(float) moveUpDistance / deltaX;
yueg81a77ff2017-12-05 10:29:03 -0800263 if (isDrawingFromRight()) {
264 deltaX = -deltaX;
265 }
yueg0b4755c2017-12-18 10:01:03 -0800266 ValueAnimator xValueAnimator =
267 createBubbleMoveAnimator(
268 windowParams.x - deltaX, windowParams.x, windowParams.y, k);
yueg81a77ff2017-12-05 10:29:03 -0800269
yueg0b4755c2017-12-18 10:01:03 -0800270 // Show expanded view
271 expandedView.setVisibility(View.VISIBLE);
yueg81a77ff2017-12-05 10:29:03 -0800272
yueg0b4755c2017-12-18 10:01:03 -0800273 // Animator 2: reveal expanded view from top left or top right
274 View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
275 ValueAnimator revealAnim =
276 createOpenCloseOutlineProvider(expandedMenu)
277 .createRevealAnimator(expandedMenu, false);
278 revealAnim.setInterpolator(accelerateDecelerateInterpolator);
279
280 // Animator 3: expanded view fade in
281 Animator fadeIn = ObjectAnimator.ofFloat(expandedView, "alpha", 0, 1);
282 fadeIn.setInterpolator(accelerateDecelerateInterpolator);
283
284 // Play all animation together
285 AnimatorSet expandAnimatorSet = new AnimatorSet();
286 expandAnimatorSet.playTogether(revealAnim, fadeIn, xValueAnimator);
287 expandAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
288 expandAnimatorSet.addListener(
yueg87111362017-12-08 12:45:50 -0800289 new AnimatorListenerAdapter() {
yueg81a77ff2017-12-05 10:29:03 -0800290 @Override
291 public void onAnimationEnd(Animator animation) {
yueg0b4755c2017-12-18 10:01:03 -0800292 // Show arrow after animation
293 viewHolder.getArrow().setVisibility(View.VISIBLE);
294 // Safe to click primary button now
295 viewHolder.setPrimaryButtonClickable(true);
yueg81a77ff2017-12-05 10:29:03 -0800296 }
yueg81a77ff2017-12-05 10:29:03 -0800297 });
yueg0b4755c2017-12-18 10:01:03 -0800298 expandAnimatorSet.start();
yueg81a77ff2017-12-05 10:29:03 -0800299
Eric Erfanian938468d2017-10-24 14:05:52 -0700300 expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
Eric Erfanian938468d2017-10-24 14:05:52 -0700301 return false;
302 }
303 });
304 setFocused(true);
305 expanded = true;
306 }
307
yueg81a77ff2017-12-05 10:29:03 -0800308 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
yuegf539f782017-12-18 16:20:58 -0800309 public void startCollapse(@CollapseEnd int endAction, boolean shouldRecoverYPosition) {
yueg81a77ff2017-12-05 10:29:03 -0800310 View expandedView = viewHolder.getExpandedView();
311 if (expandedView.getVisibility() != View.VISIBLE || collapseAnimation != null) {
312 // Drawer is already collapsed or animation is running.
313 return;
314 }
315
316 overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT;
317 setFocused(false);
318
319 if (collapseEndAction == CollapseEnd.NOTHING) {
320 collapseEndAction = endAction;
321 }
yuega235e132017-12-13 14:13:57 -0800322 setPrimaryButtonAccessibilityAction(
323 context.getString(R.string.a11y_bubble_primary_button_expand_action));
yueg0b4755c2017-12-18 10:01:03 -0800324
325 // Hide arrow before animation
326 viewHolder.getArrow().setVisibility(View.INVISIBLE);
327
328 // No click during animation to avoid jank.
329 viewHolder.setPrimaryButtonClickable(false);
330
331 // Calculate animation values
332 int deltaX = (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
333 float k =
334 (savedYPosition != -1 && shouldRecoverYPosition)
335 ? (savedYPosition - windowParams.y) / (float) deltaX
336 : 0;
337 // The position is not useful after collapse
338 savedYPosition = -1;
339
340 // Animation 1: animate x-move and y-move (if needed) together
341 ValueAnimator xValueAnimator =
342 createBubbleMoveAnimator(windowParams.x, windowParams.x - deltaX, windowParams.y, k);
343
344 // Animator 2: hide expanded view to top left or top right
345 View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
346 ValueAnimator revealAnim =
347 createOpenCloseOutlineProvider(expandedMenu).createRevealAnimator(expandedMenu, true);
348 revealAnim.setInterpolator(accelerateDecelerateInterpolator);
349
350 // Animator 3: expanded view fade out
351 Animator fadeOut = ObjectAnimator.ofFloat(expandedView, "alpha", 1, 0);
352 fadeOut.setInterpolator(accelerateDecelerateInterpolator);
353
354 // Play all animation together
355 AnimatorSet collapseAnimatorSet = new AnimatorSet();
356 collapseAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
357 collapseAnimatorSet.playTogether(revealAnim, fadeOut, xValueAnimator);
358 collapseAnimatorSet.addListener(
359 new AnimatorListenerAdapter() {
360 @Override
361 public void onAnimationEnd(Animator animation) {
362 collapseAnimation = null;
363 expanded = false;
364
365 if (textShowing) {
366 // Will do resize once the text is done.
367 return;
368 }
369
370 // If this collapse was to come before a hide, do it now.
371 if (collapseEndAction == CollapseEnd.HIDE) {
372 hide();
373 }
374 collapseEndAction = CollapseEnd.NOTHING;
375
376 // If collapse on the right side, the primary button move left a bit after drawer
377 // visibility becoming GONE. To avoid it, we create a new ViewHolder.
378 // It also set primary button clickable back to true, so no need to reset manually.
379 replaceViewHolder();
380
381 // Resume normal gravity after any resizing is done.
382 handler.postDelayed(
yueg81a77ff2017-12-05 10:29:03 -0800383 () -> {
yueg0b4755c2017-12-18 10:01:03 -0800384 overrideGravity = null;
385 if (!viewHolder.isMoving()) {
386 viewHolder.undoGravityOverride();
yueg81a77ff2017-12-05 10:29:03 -0800387 }
yueg0b4755c2017-12-18 10:01:03 -0800388 },
389 // Need to wait twice as long for resize and layout
390 WINDOW_REDRAW_DELAY_MILLIS * 2);
391 }
392 });
393 collapseAnimatorSet.start();
yueg81a77ff2017-12-05 10:29:03 -0800394 }
395
Eric Erfanian938468d2017-10-24 14:05:52 -0700396 /**
397 * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is
398 * already showing this method does nothing.
399 */
400 public void show() {
401 if (collapseEndAction == CollapseEnd.HIDE) {
402 // If show() was called while collapsing, make sure we don't hide after.
403 collapseEndAction = CollapseEnd.NOTHING;
404 }
405 if (visibility == Visibility.SHOWING || visibility == Visibility.ENTERING) {
406 return;
407 }
408
409 hideAfterText = false;
410
yueg07e323c2017-12-19 16:05:47 -0800411 boolean isRtl =
412 TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
Eric Erfanian938468d2017-10-24 14:05:52 -0700413 if (windowParams == null) {
414 // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O.
415 @SuppressWarnings("deprecation")
416 @SuppressLint("InlinedApi")
417 int type =
418 BuildCompat.isAtLeastO()
419 ? LayoutParams.TYPE_APPLICATION_OVERLAY
420 : LayoutParams.TYPE_PHONE;
421
422 windowParams =
423 new LayoutParams(
424 type,
425 LayoutParams.FLAG_NOT_TOUCH_MODAL
426 | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
427 | LayoutParams.FLAG_NOT_FOCUSABLE
428 | LayoutParams.FLAG_LAYOUT_NO_LIMITS,
429 PixelFormat.TRANSLUCENT);
yueg07e323c2017-12-19 16:05:47 -0800430 windowParams.gravity = Gravity.TOP | (isRtl ? Gravity.RIGHT : Gravity.LEFT);
yueg81a77ff2017-12-05 10:29:03 -0800431 windowParams.x = leftBoundary;
Eric Erfanian938468d2017-10-24 14:05:52 -0700432 windowParams.y = currentInfo.getStartingYPosition();
433 windowParams.height = LayoutParams.WRAP_CONTENT;
434 windowParams.width = LayoutParams.WRAP_CONTENT;
435 }
436
yueg87111362017-12-08 12:45:50 -0800437 if (exitAnimatorSet != null) {
438 exitAnimatorSet.removeAllListeners();
439 exitAnimatorSet.cancel();
440 exitAnimatorSet = null;
Eric Erfanian938468d2017-10-24 14:05:52 -0700441 } else {
442 windowManager.addView(viewHolder.getRoot(), windowParams);
yueg87111362017-12-08 12:45:50 -0800443 viewHolder.getPrimaryButton().setVisibility(View.VISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700444 viewHolder.getPrimaryButton().setScaleX(0);
445 viewHolder.getPrimaryButton().setScaleY(0);
yueg87111362017-12-08 12:45:50 -0800446 viewHolder.getPrimaryAvatar().setAlpha(0f);
447 viewHolder.getPrimaryIcon().setAlpha(0f);
yueg07e323c2017-12-19 16:05:47 -0800448 if (isRtl) {
449 onLeftRightSwitch(true);
450 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700451 }
452
453 viewHolder.setChildClickable(true);
454 visibility = Visibility.ENTERING;
yueg87111362017-12-08 12:45:50 -0800455
yuega235e132017-12-13 14:13:57 -0800456 setPrimaryButtonAccessibilityAction(
457 context.getString(R.string.a11y_bubble_primary_button_expand_action));
458
yueg87111362017-12-08 12:45:50 -0800459 // Show bubble animation: scale the whole bubble to 1, and change avatar+icon's alpha to 1
460 ObjectAnimator scaleXAnimator =
461 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 1);
462 ObjectAnimator scaleYAnimator =
463 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 1);
464 ObjectAnimator avatarAlphaAnimator =
465 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 1);
466 ObjectAnimator iconAlphaAnimator =
467 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 1);
468 AnimatorSet enterAnimatorSet = new AnimatorSet();
469 enterAnimatorSet.playTogether(
470 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
471 enterAnimatorSet.setInterpolator(new OvershootInterpolator());
472 enterAnimatorSet.addListener(
473 new AnimatorListenerAdapter() {
474 @Override
475 public void onAnimationEnd(Animator animation) {
476 visibility = Visibility.SHOWING;
477 // Show the queued up text, if available.
478 if (textAfterShow != null) {
479 showText(textAfterShow);
480 textAfterShow = null;
481 }
482 }
483 });
484 enterAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700485
486 updatePrimaryIconAnimation();
487 }
488
489 /** Hide the bubble. */
490 public void hide() {
491 if (hideAfterText) {
492 // hideAndReset() will be called after showing text, do nothing here.
493 return;
494 }
495 hideHelper(this::defaultAfterHidingAnimation);
496 }
497
498 /** Hide the bubble and reset {@viewHolder} to initial state */
499 public void hideAndReset() {
500 hideHelper(
501 () -> {
502 defaultAfterHidingAnimation();
503 reset();
504 });
505 }
506
507 /** Returns whether the bubble is currently visible */
508 public boolean isVisible() {
509 return visibility == Visibility.SHOWING
510 || visibility == Visibility.ENTERING
511 || visibility == Visibility.EXITING;
512 }
513
514 /**
515 * Set the info for this Bubble to display
516 *
517 * @param bubbleInfo the BubbleInfo to display in this Bubble.
518 */
519 public void setBubbleInfo(@NonNull NewBubbleInfo bubbleInfo) {
520 currentInfo = bubbleInfo;
521 update();
522 }
523
524 /**
525 * Update the state and behavior of actions.
526 *
527 * @param actions the new state of the bubble's actions
528 */
529 public void updateActions(@NonNull List<Action> actions) {
530 currentInfo = NewBubbleInfo.from(currentInfo).setActions(actions).build();
531 updateButtonStates();
532 }
533
yuega5a08d82017-10-31 14:11:53 -0700534 /**
535 * Update the avatar from photo.
536 *
537 * @param avatar the new photo avatar in the bubble's primary button
538 */
539 public void updatePhotoAvatar(@NonNull Drawable avatar) {
540 // Make it round
541 int bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
542 Drawable roundAvatar =
543 DrawableConverter.getRoundedDrawable(context, avatar, bubbleSize, bubbleSize);
544
545 updateAvatar(roundAvatar);
546 }
547
548 /**
549 * Update the avatar.
550 *
551 * @param avatar the new avatar in the bubble's primary button
552 */
553 public void updateAvatar(@NonNull Drawable avatar) {
554 if (!avatar.equals(currentInfo.getAvatar())) {
555 currentInfo = NewBubbleInfo.from(currentInfo).setAvatar(avatar).build();
556 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
557 }
558 }
559
Eric Erfanian938468d2017-10-24 14:05:52 -0700560 /** Returns the currently displayed NewBubbleInfo */
561 public NewBubbleInfo getBubbleInfo() {
562 return currentInfo;
563 }
564
565 /**
566 * Display text in the main bubble. The bubble's drawer is not expandable while text is showing,
567 * and the drawer will be closed if already open.
568 *
569 * @param text the text to display to the user
570 */
571 public void showText(@NonNull CharSequence text) {
572 textShowing = true;
573 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800574 startCollapse(CollapseEnd.NOTHING, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700575 doShowText(text);
576 } else {
577 // Need to transition from old bounds to new bounds manually
578 NewChangeOnScreenBounds transition = new NewChangeOnScreenBounds();
579 // Prepare and capture start values
580 TransitionValues startValues = new TransitionValues();
581 startValues.view = viewHolder.getPrimaryButton();
582 transition.addTarget(startValues.view);
583 transition.captureStartValues(startValues);
584
585 // If our view is not laid out yet, postpone showing the text.
586 if (startValues.values.isEmpty()) {
587 textAfterShow = text;
588 return;
589 }
590
yueg81a77ff2017-12-05 10:29:03 -0800591 doShowText(text);
592 // Hide the text so we can animate it in
593 viewHolder.getPrimaryText().setAlpha(0);
Eric Erfanian938468d2017-10-24 14:05:52 -0700594
yueg81a77ff2017-12-05 10:29:03 -0800595 ViewAnimator primaryButton = viewHolder.getPrimaryButton();
596 // Cancel the automatic transition scheduled in doShowText
597 TransitionManager.endTransitions((ViewGroup) primaryButton.getParent());
598 primaryButton
599 .getViewTreeObserver()
600 .addOnPreDrawListener(
601 new OnPreDrawListener() {
602 @Override
603 public boolean onPreDraw() {
604 primaryButton.getViewTreeObserver().removeOnPreDrawListener(this);
Eric Erfanian938468d2017-10-24 14:05:52 -0700605
yueg81a77ff2017-12-05 10:29:03 -0800606 // Prepare and capture end values, always use the size of primaryText since
607 // its invisibility makes primaryButton smaller than expected
608 TransitionValues endValues = new TransitionValues();
609 endValues.values.put(
610 NewChangeOnScreenBounds.PROPNAME_WIDTH,
611 viewHolder.getPrimaryText().getWidth());
612 endValues.values.put(
613 NewChangeOnScreenBounds.PROPNAME_HEIGHT,
614 viewHolder.getPrimaryText().getHeight());
615 endValues.view = primaryButton;
616 transition.addTarget(endValues.view);
617 transition.captureEndValues(endValues);
Eric Erfanian938468d2017-10-24 14:05:52 -0700618
yueg81a77ff2017-12-05 10:29:03 -0800619 // animate the primary button bounds change
620 Animator bounds =
621 transition.createAnimator(primaryButton, startValues, endValues);
Eric Erfanian938468d2017-10-24 14:05:52 -0700622
yueg81a77ff2017-12-05 10:29:03 -0800623 // Animate the text in
624 Animator alpha =
625 ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f);
Eric Erfanian938468d2017-10-24 14:05:52 -0700626
yueg81a77ff2017-12-05 10:29:03 -0800627 AnimatorSet set = new AnimatorSet();
628 set.play(bounds).before(alpha);
629 set.start();
630 return false;
631 }
632 });
Eric Erfanian938468d2017-10-24 14:05:52 -0700633 }
634 handler.removeCallbacks(collapseRunnable);
635 handler.postDelayed(collapseRunnable, SHOW_TEXT_DURATION_MILLIS);
636 }
637
Eric Erfanian938468d2017-10-24 14:05:52 -0700638 @Nullable
639 Integer getGravityOverride() {
640 return overrideGravity;
641 }
642
643 void onMoveStart() {
yueg81a77ff2017-12-05 10:29:03 -0800644 if (viewHolder.getExpandedView().getVisibility() == View.VISIBLE) {
645 viewHolder.setDrawerVisibility(View.INVISIBLE);
646 }
647 expanded = false;
648 savedYPosition = -1;
649
Eric Erfanian938468d2017-10-24 14:05:52 -0700650 viewHolder
651 .getPrimaryButton()
652 .animate()
653 .translationZ(
yuegc6deafc2017-11-06 16:42:13 -0800654 context
655 .getResources()
656 .getDimensionPixelOffset(R.dimen.bubble_dragging_elevation_change));
Eric Erfanian938468d2017-10-24 14:05:52 -0700657 }
658
659 void onMoveFinish() {
660 viewHolder.getPrimaryButton().animate().translationZ(0);
Eric Erfanian938468d2017-10-24 14:05:52 -0700661 }
662
663 void primaryButtonClick() {
664 if (textShowing || currentInfo.getActions().isEmpty()) {
665 return;
666 }
667 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800668 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
669 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700670 } else {
yuegf539f782017-12-18 16:20:58 -0800671 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_EXPAND);
672 expand();
Eric Erfanian938468d2017-10-24 14:05:52 -0700673 }
674 }
675
yueg81a77ff2017-12-05 10:29:03 -0800676 void onLeftRightSwitch(boolean onRight) {
yueg87111362017-12-08 12:45:50 -0800677 // Move primary icon to the other side so it's not partially hiden
yueg81a77ff2017-12-05 10:29:03 -0800678 View primaryIcon = viewHolder.getPrimaryIcon();
yueg87111362017-12-08 12:45:50 -0800679 primaryIcon.animate().translationX(onRight ? -primaryIconMoveDistance : 0).start();
yueg81a77ff2017-12-05 10:29:03 -0800680 }
681
Eric Erfanian938468d2017-10-24 14:05:52 -0700682 LayoutParams getWindowParams() {
683 return windowParams;
684 }
685
686 View getRootView() {
687 return viewHolder.getRoot();
688 }
689
690 /**
691 * Hide the bubble if visible. Will run a short exit animation and before hiding, and {@code
692 * afterHiding} after hiding. If the bubble is currently showing text, will hide after the text is
693 * done displaying. If the bubble is not visible this method does nothing.
694 */
yueg87111362017-12-08 12:45:50 -0800695 @VisibleForTesting
696 void hideHelper(Runnable afterHiding) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700697 if (visibility == Visibility.HIDDEN || visibility == Visibility.EXITING) {
698 return;
699 }
700
701 // Make bubble non clickable to prevent further buggy actions
702 viewHolder.setChildClickable(false);
703
704 if (textShowing) {
705 hideAfterText = true;
706 return;
707 }
708
709 if (collapseAnimation != null) {
710 collapseEndAction = CollapseEnd.HIDE;
711 return;
712 }
713
714 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800715 startCollapse(CollapseEnd.HIDE, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700716 return;
717 }
718
719 visibility = Visibility.EXITING;
yueg87111362017-12-08 12:45:50 -0800720
721 // Hide bubble animation: scale the whole bubble to 0, and change avatar+icon's alpha to 0
722 ObjectAnimator scaleXAnimator =
723 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 0);
724 ObjectAnimator scaleYAnimator =
725 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 0);
726 ObjectAnimator avatarAlphaAnimator =
727 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 0);
728 ObjectAnimator iconAlphaAnimator =
729 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 0);
730 exitAnimatorSet = new AnimatorSet();
731 exitAnimatorSet.playTogether(
732 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
733 exitAnimatorSet.setInterpolator(new AnticipateInterpolator());
734 exitAnimatorSet.addListener(
735 new AnimatorListenerAdapter() {
736 @Override
yuega235e132017-12-13 14:13:57 -0800737 public void onAnimationStart(Animator animation) {
738 viewHolder.getPrimaryButton().setAccessibilityDelegate(null);
739 }
740
741 @Override
yueg87111362017-12-08 12:45:50 -0800742 public void onAnimationEnd(Animator animation) {
743 afterHiding.run();
744 }
745 });
746 exitAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700747 }
748
749 private void reset() {
750 viewHolder = new ViewHolder(viewHolder.getRoot().getContext());
751 update();
752 }
753
754 private void update() {
yuega5a08d82017-10-31 14:11:53 -0700755 // Whole primary button background
yueg84ac49b2017-11-01 16:22:28 -0700756 Drawable backgroundCirle =
757 context.getResources().getDrawable(R.drawable.bubble_shape_circle, context.getTheme());
Eric Erfanian938468d2017-10-24 14:05:52 -0700758 int primaryTint =
759 ColorUtils.compositeColors(
760 context.getColor(R.color.bubble_primary_background_darken),
761 currentInfo.getPrimaryColor());
yueg84ac49b2017-11-01 16:22:28 -0700762 backgroundCirle.mutate().setTint(primaryTint);
763 viewHolder.getPrimaryButton().setBackground(backgroundCirle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700764
yuega5a08d82017-10-31 14:11:53 -0700765 // Small icon
yueg84ac49b2017-11-01 16:22:28 -0700766 Drawable smallIconBackgroundCircle =
767 context
768 .getResources()
769 .getDrawable(R.drawable.bubble_shape_circle_small, context.getTheme());
770 smallIconBackgroundCircle.setTint(context.getColor(R.color.bubble_button_color_blue));
771 viewHolder.getPrimaryIcon().setBackground(smallIconBackgroundCircle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700772 viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon());
yuega5a08d82017-10-31 14:11:53 -0700773 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
Eric Erfanian938468d2017-10-24 14:05:52 -0700774
yuega5a08d82017-10-31 14:11:53 -0700775 updatePrimaryIconAnimation();
Eric Erfanian938468d2017-10-24 14:05:52 -0700776 updateButtonStates();
777 }
778
779 private void updatePrimaryIconAnimation() {
780 Drawable drawable = viewHolder.getPrimaryIcon().getDrawable();
781 if (drawable instanceof Animatable) {
782 if (isVisible()) {
783 ((Animatable) drawable).start();
784 } else {
785 ((Animatable) drawable).stop();
786 }
787 }
788 }
789
790 private void updateButtonStates() {
yueg84ac49b2017-11-01 16:22:28 -0700791 configureButton(currentInfo.getActions().get(0), viewHolder.getFullScreenButton());
792 configureButton(currentInfo.getActions().get(1), viewHolder.getMuteButton());
793 configureButton(currentInfo.getActions().get(2), viewHolder.getAudioRouteButton());
794 configureButton(currentInfo.getActions().get(3), viewHolder.getEndCallButton());
Eric Erfanian938468d2017-10-24 14:05:52 -0700795 }
796
yueg87111362017-12-08 12:45:50 -0800797 @VisibleForTesting
798 void doShowText(@NonNull CharSequence text) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700799 TransitionManager.beginDelayedTransition((ViewGroup) viewHolder.getPrimaryButton().getParent());
800 viewHolder.getPrimaryText().setText(text);
801 viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_TEXT);
802 }
803
yueg84ac49b2017-11-01 16:22:28 -0700804 private void configureButton(Action action, NewCheckableButton button) {
yueg07e323c2017-12-19 16:05:47 -0800805 boolean isRtl =
806 TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
807 if (isRtl) {
808 button.setCompoundDrawablesWithIntrinsicBounds(null, null, action.getIconDrawable(), null);
809 } else {
810 button.setCompoundDrawablesWithIntrinsicBounds(action.getIconDrawable(), null, null, null);
811 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700812 button.setChecked(action.isChecked());
813 button.setEnabled(action.isEnabled());
yueg84ac49b2017-11-01 16:22:28 -0700814 button.setText(action.getName());
yuega235e132017-12-13 14:13:57 -0800815 button.setContentDescription(action.getName());
Eric Erfanian938468d2017-10-24 14:05:52 -0700816 button.setOnClickListener(v -> doAction(action));
817 }
818
819 private void doAction(Action action) {
820 try {
821 action.getIntent().send();
822 } catch (CanceledException e) {
823 throw new RuntimeException(e);
824 }
825 }
826
yueg81a77ff2017-12-05 10:29:03 -0800827 /**
828 * Create a new ViewHolder object to replace the old one.It only happens when not moving and
829 * collapsed.
830 */
831 void replaceViewHolder() {
832 LogUtil.enterBlock("NewBubble.replaceViewHolder");
Eric Erfanian938468d2017-10-24 14:05:52 -0700833 ViewHolder oldViewHolder = viewHolder;
Eric Erfanian938468d2017-10-24 14:05:52 -0700834
yueg81a77ff2017-12-05 10:29:03 -0800835 // Create a new ViewHolder and copy needed info.
836 viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext());
837 viewHolder
838 .getPrimaryButton()
839 .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild());
840 viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText());
yueg87111362017-12-08 12:45:50 -0800841 viewHolder.getPrimaryIcon().setX(isDrawingFromRight() ? 0 : primaryIconMoveDistance);
yueg81a77ff2017-12-05 10:29:03 -0800842 viewHolder
843 .getPrimaryIcon()
yueg87111362017-12-08 12:45:50 -0800844 .setTranslationX(isDrawingFromRight() ? -primaryIconMoveDistance : 0);
yuega235e132017-12-13 14:13:57 -0800845 setPrimaryButtonAccessibilityAction(
846 context.getString(R.string.a11y_bubble_primary_button_expand_action));
Eric Erfanian938468d2017-10-24 14:05:52 -0700847
yueg81a77ff2017-12-05 10:29:03 -0800848 update();
849
850 // Add new view at its horizontal boundary
Eric Erfanian938468d2017-10-24 14:05:52 -0700851 ViewGroup root = viewHolder.getRoot();
yueg81a77ff2017-12-05 10:29:03 -0800852 windowParams.x = leftBoundary;
853 windowParams.gravity = Gravity.TOP | (isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT);
Eric Erfanian938468d2017-10-24 14:05:52 -0700854 windowManager.addView(root, windowParams);
yueg81a77ff2017-12-05 10:29:03 -0800855
856 // Remove the old view after delay
Eric Erfanian938468d2017-10-24 14:05:52 -0700857 root.getViewTreeObserver()
858 .addOnPreDrawListener(
859 new OnPreDrawListener() {
860 @Override
861 public boolean onPreDraw() {
862 root.getViewTreeObserver().removeOnPreDrawListener(this);
863 // Wait a bit before removing the old view; make sure the new one has drawn over it.
864 handler.postDelayed(
865 () -> windowManager.removeView(oldViewHolder.getRoot()),
866 WINDOW_REDRAW_DELAY_MILLIS);
867 return true;
868 }
869 });
870 }
871
yueg81a77ff2017-12-05 10:29:03 -0800872 int getDrawerVisibility() {
873 return viewHolder.getExpandedView().getVisibility();
Eric Erfanian938468d2017-10-24 14:05:52 -0700874 }
875
876 private boolean isDrawingFromRight() {
877 return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
878 }
879
880 private void setFocused(boolean focused) {
881 if (focused) {
882 windowParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
883 } else {
884 windowParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
885 }
886 windowManager.updateViewLayout(getRootView(), windowParams);
887 }
888
889 private void defaultAfterHidingAnimation() {
yueg87111362017-12-08 12:45:50 -0800890 exitAnimatorSet = null;
891 viewHolder.getPrimaryButton().setVisibility(View.INVISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700892 windowManager.removeView(viewHolder.getRoot());
893 visibility = Visibility.HIDDEN;
894
895 updatePrimaryIconAnimation();
896 }
897
yueg81a77ff2017-12-05 10:29:03 -0800898 private void logBasicOrCallImpression(DialerImpression.Type impressionType) {
yuegf539f782017-12-18 16:20:58 -0800899 // Bubble is shown for outgoing, active or background call
900 DialerCall call = CallList.getInstance().getOutgoingCall();
901 if (call == null) {
902 call = CallList.getInstance().getActiveOrBackgroundCall();
903 }
yueg81a77ff2017-12-05 10:29:03 -0800904 if (call != null) {
905 Logger.get(context)
906 .logCallImpression(impressionType, call.getUniqueCallId(), call.getTimeAddedMs());
907 } else {
908 Logger.get(context).logImpression(impressionType);
909 }
910 }
911
yuega235e132017-12-13 14:13:57 -0800912 private void setPrimaryButtonAccessibilityAction(String description) {
913 viewHolder
914 .getPrimaryButton()
915 .setAccessibilityDelegate(
916 new AccessibilityDelegate() {
917 @Override
918 public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) {
919 super.onInitializeAccessibilityNodeInfo(v, info);
920
921 AccessibilityAction clickAction =
922 new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, description);
923 info.addAction(clickAction);
924 }
925 });
926 }
927
yueg0b4755c2017-12-18 10:01:03 -0800928 private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider(View view) {
929 int startRectX = isDrawingFromRight() ? view.getMeasuredWidth() : 0;
930 Rect startRect = new Rect(startRectX, 0, startRectX, 0);
931 Rect endRect = new Rect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
932
933 float bubbleRadius = context.getResources().getDimension(R.dimen.bubble_radius);
934 return new RoundedRectRevealOutlineProvider(bubbleRadius, bubbleRadius, startRect, endRect);
935 }
936
937 private ValueAnimator createBubbleMoveAnimator(int startX, int endX, int startY, float k) {
938 ValueAnimator xValueAnimator = ValueAnimator.ofFloat(startX, endX);
939 xValueAnimator.setInterpolator(new LinearOutSlowInInterpolator());
940 xValueAnimator.addUpdateListener(
941 (valueAnimator) -> {
942 // Update windowParams and the root layout.
943 // We can't do ViewPropertyAnimation since it clips children.
944 float newX = (float) valueAnimator.getAnimatedValue();
945 if (k != 0) {
946 windowParams.y = startY + (int) (Math.abs(newX - (float) startX) * k);
947 }
948 windowParams.x = (int) newX;
949 windowManager.updateViewLayout(viewHolder.getRoot(), windowParams);
950 });
951 return xValueAnimator;
952 }
953
Eric Erfanian938468d2017-10-24 14:05:52 -0700954 @VisibleForTesting
955 class ViewHolder {
956
yuega5a08d82017-10-31 14:11:53 -0700957 public static final int CHILD_INDEX_AVATAR_AND_ICON = 0;
Eric Erfanian938468d2017-10-24 14:05:52 -0700958 public static final int CHILD_INDEX_TEXT = 1;
959
yueg0b4755c2017-12-18 10:01:03 -0800960 private NewMoveHandler moveHandler;
Eric Erfanian938468d2017-10-24 14:05:52 -0700961 private final NewWindowRoot root;
962 private final ViewAnimator primaryButton;
963 private final ImageView primaryIcon;
yuega5a08d82017-10-31 14:11:53 -0700964 private final ImageView primaryAvatar;
Eric Erfanian938468d2017-10-24 14:05:52 -0700965 private final TextView primaryText;
yueg0b4755c2017-12-18 10:01:03 -0800966 private final View arrow;
Eric Erfanian938468d2017-10-24 14:05:52 -0700967
968 private final NewCheckableButton fullScreenButton;
969 private final NewCheckableButton muteButton;
970 private final NewCheckableButton audioRouteButton;
971 private final NewCheckableButton endCallButton;
972 private final View expandedView;
973
974 public ViewHolder(Context context) {
975 // Window root is not in the layout file so that the inflater has a view to inflate into
976 this.root = new NewWindowRoot(context);
977 LayoutInflater inflater = LayoutInflater.from(root.getContext());
978 View contentView = inflater.inflate(R.layout.new_bubble_base, root, true);
979 expandedView = contentView.findViewById(R.id.bubble_expanded_layout);
980 primaryButton = contentView.findViewById(R.id.bubble_button_primary);
yuega5a08d82017-10-31 14:11:53 -0700981 primaryAvatar = contentView.findViewById(R.id.bubble_icon_avatar);
Eric Erfanian938468d2017-10-24 14:05:52 -0700982 primaryIcon = contentView.findViewById(R.id.bubble_icon_primary);
983 primaryText = contentView.findViewById(R.id.bubble_text);
yueg0b4755c2017-12-18 10:01:03 -0800984 arrow = contentView.findViewById(R.id.bubble_triangle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700985
986 fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen);
987 muteButton = contentView.findViewById(R.id.bubble_button_mute);
988 audioRouteButton = contentView.findViewById(R.id.bubble_button_audio_route);
989 endCallButton = contentView.findViewById(R.id.bubble_button_end_call);
990
991 root.setOnBackPressedListener(
992 () -> {
993 if (visibility == Visibility.SHOWING && expanded) {
yuegf539f782017-12-18 16:20:58 -0800994 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
995 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700996 return true;
997 }
998 return false;
999 });
1000 root.setOnConfigurationChangedListener(
1001 (configuration) -> {
1002 // The values in the current MoveHandler may be stale, so replace it. Then ensure the
1003 // Window is in bounds
1004 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
1005 moveHandler.snapToBounds();
1006 });
1007 root.setOnTouchListener(
1008 (v, event) -> {
1009 if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
yuegf539f782017-12-18 16:20:58 -08001010 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
1011 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -07001012 return true;
1013 }
1014 return false;
1015 });
1016 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
1017 }
1018
1019 private void setChildClickable(boolean clickable) {
1020 fullScreenButton.setClickable(clickable);
1021 muteButton.setClickable(clickable);
1022 audioRouteButton.setClickable(clickable);
1023 endCallButton.setClickable(clickable);
yueg0b4755c2017-12-18 10:01:03 -08001024 setPrimaryButtonClickable(clickable);
1025 }
Eric Erfanian938468d2017-10-24 14:05:52 -07001026
yueg0b4755c2017-12-18 10:01:03 -08001027 private void setPrimaryButtonClickable(boolean clickable) {
Eric Erfanian938468d2017-10-24 14:05:52 -07001028 moveHandler.setClickable(clickable);
1029 }
1030
yueg81a77ff2017-12-05 10:29:03 -08001031 public int getMoveUpDistance() {
1032 int deltaAllowed =
1033 expandedView.getHeight()
1034 - context
1035 .getResources()
1036 .getDimensionPixelOffset(R.dimen.bubble_button_padding_vertical)
1037 * 2;
1038 return moveHandler.getMoveUpDistance(deltaAllowed);
1039 }
1040
Eric Erfanian938468d2017-10-24 14:05:52 -07001041 public ViewGroup getRoot() {
1042 return root;
1043 }
1044
1045 public ViewAnimator getPrimaryButton() {
1046 return primaryButton;
1047 }
1048
1049 public ImageView getPrimaryIcon() {
1050 return primaryIcon;
1051 }
1052
yuega5a08d82017-10-31 14:11:53 -07001053 public ImageView getPrimaryAvatar() {
1054 return primaryAvatar;
1055 }
1056
Eric Erfanian938468d2017-10-24 14:05:52 -07001057 public TextView getPrimaryText() {
1058 return primaryText;
1059 }
1060
1061 public View getExpandedView() {
1062 return expandedView;
1063 }
1064
yueg0b4755c2017-12-18 10:01:03 -08001065 public View getArrow() {
1066 return arrow;
1067 }
1068
Eric Erfanian938468d2017-10-24 14:05:52 -07001069 public NewCheckableButton getFullScreenButton() {
1070 return fullScreenButton;
1071 }
1072
1073 public NewCheckableButton getMuteButton() {
1074 return muteButton;
1075 }
1076
1077 public NewCheckableButton getAudioRouteButton() {
1078 return audioRouteButton;
1079 }
1080
1081 public NewCheckableButton getEndCallButton() {
1082 return endCallButton;
1083 }
1084
1085 public void setDrawerVisibility(int visibility) {
1086 expandedView.setVisibility(visibility);
1087 }
1088
1089 public boolean isMoving() {
1090 return moveHandler.isMoving();
1091 }
1092
1093 public void undoGravityOverride() {
1094 moveHandler.undoGravityOverride();
1095 }
1096 }
Eric Erfanian938468d2017-10-24 14:05:52 -07001097}