blob: ef3a971dd5de63fd57c8031c0468662e73e86e15 [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;
yueg81a77ff2017-12-05 10:29:03 -080020import android.animation.Animator.AnimatorListener;
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;
yueg81a77ff2017-12-05 10:29:03 -080028import android.graphics.Path;
Eric Erfanian938468d2017-10-24 14:05:52 -070029import android.graphics.PixelFormat;
30import 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;
41import android.support.v4.view.animation.FastOutLinearInInterpolator;
42import android.support.v4.view.animation.LinearOutSlowInInterpolator;
43import 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;
50import android.view.ViewGroup;
51import android.view.ViewPropertyAnimator;
52import android.view.ViewTreeObserver.OnPreDrawListener;
53import android.view.WindowManager;
54import android.view.WindowManager.LayoutParams;
55import android.view.animation.AnticipateInterpolator;
56import android.view.animation.OvershootInterpolator;
yueg81a77ff2017-12-05 10:29:03 -080057import android.widget.FrameLayout;
Eric Erfanian938468d2017-10-24 14:05:52 -070058import android.widget.ImageView;
59import android.widget.TextView;
60import android.widget.ViewAnimator;
yueg81a77ff2017-12-05 10:29:03 -080061import com.android.dialer.common.LogUtil;
62import com.android.dialer.logging.DialerImpression;
63import com.android.dialer.logging.Logger;
yuega5a08d82017-10-31 14:11:53 -070064import com.android.dialer.util.DrawableConverter;
yueg81a77ff2017-12-05 10:29:03 -080065import com.android.incallui.call.CallList;
66import com.android.incallui.call.DialerCall;
Eric Erfanian938468d2017-10-24 14:05:52 -070067import com.android.newbubble.NewBubbleInfo.Action;
68import java.lang.annotation.Retention;
69import java.lang.annotation.RetentionPolicy;
70import java.util.List;
71
72/**
73 * Creates and manages a bubble window from information in a {@link NewBubbleInfo}. Before creating,
74 * be sure to check whether bubbles may be shown using {@link #canShowBubbles(Context)} and request
75 * permission if necessary ({@link #getRequestPermissionIntent(Context)} is provided for
76 * convenience)
77 */
78public class NewBubble {
79 // This class has some odd behavior that is not immediately obvious in order to avoid jank when
80 // resizing. See http://go/bubble-resize for details.
81
82 // How long text should show after showText(CharSequence) is called
83 private static final int SHOW_TEXT_DURATION_MILLIS = 3000;
84 // How long the new window should show before destroying the old one during resize operations.
85 // This ensures the new window has had time to draw first.
86 private static final int WINDOW_REDRAW_DELAY_MILLIS = 50;
87
88 private static Boolean canShowBubblesForTesting = null;
89
90 private final Context context;
91 private final WindowManager windowManager;
92
93 private final Handler handler;
94 private LayoutParams windowParams;
95
96 // Initialized in factory method
97 @SuppressWarnings("NullableProblems")
98 @NonNull
99 private NewBubbleInfo currentInfo;
100
101 @Visibility private int visibility;
102 private boolean expanded;
103 private boolean textShowing;
104 private boolean hideAfterText;
105 private CharSequence textAfterShow;
106 private int collapseEndAction;
107
yueg81a77ff2017-12-05 10:29:03 -0800108 ViewHolder viewHolder;
Eric Erfanian938468d2017-10-24 14:05:52 -0700109 private ViewPropertyAnimator collapseAnimation;
110 private Integer overrideGravity;
111 private ViewPropertyAnimator exitAnimator;
112
yueg81a77ff2017-12-05 10:29:03 -0800113 private int leftBoundary;
114 private int savedYPosition = -1;
115
Eric Erfanian938468d2017-10-24 14:05:52 -0700116 private final Runnable collapseRunnable =
117 new Runnable() {
118 @Override
119 public void run() {
120 textShowing = false;
121 if (hideAfterText) {
122 // Always reset here since text shouldn't keep showing.
123 hideAndReset();
124 } else {
yueg81a77ff2017-12-05 10:29:03 -0800125 viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_AVATAR_AND_ICON);
Eric Erfanian938468d2017-10-24 14:05:52 -0700126 }
127 }
128 };
129
Eric Erfanian938468d2017-10-24 14:05:52 -0700130 /** Type of action after bubble collapse */
131 @Retention(RetentionPolicy.SOURCE)
132 @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE})
133 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
134 public @interface CollapseEnd {
135 int NOTHING = 0;
136 int HIDE = 1;
137 }
138
139 @Retention(RetentionPolicy.SOURCE)
140 @IntDef({Visibility.ENTERING, Visibility.SHOWING, Visibility.EXITING, Visibility.HIDDEN})
141 private @interface Visibility {
142 int HIDDEN = 0;
143 int ENTERING = 1;
144 int SHOWING = 2;
145 int EXITING = 3;
146 }
147
148 /** Indicate bubble expansion state. */
149 @Retention(RetentionPolicy.SOURCE)
150 @IntDef({ExpansionState.START_EXPANDING, ExpansionState.START_COLLAPSING})
151 public @interface ExpansionState {
152 // TODO(yueg): add more states when needed
153 int START_EXPANDING = 0;
154 int START_COLLAPSING = 1;
155 }
156
157 /**
158 * Determines whether bubbles can be shown based on permissions obtained. This should be checked
159 * before attempting to create a Bubble.
160 *
161 * @return true iff bubbles are able to be shown.
162 * @see Settings#canDrawOverlays(Context)
163 */
164 public static boolean canShowBubbles(@NonNull Context context) {
165 return canShowBubblesForTesting != null
166 ? canShowBubblesForTesting
167 : Settings.canDrawOverlays(context);
168 }
169
170 @VisibleForTesting(otherwise = VisibleForTesting.NONE)
171 public static void setCanShowBubblesForTesting(boolean canShowBubbles) {
172 canShowBubblesForTesting = canShowBubbles;
173 }
174
175 /** Returns an Intent to request permission to show overlays */
176 @NonNull
177 public static Intent getRequestPermissionIntent(@NonNull Context context) {
178 return new Intent(
179 Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
180 Uri.fromParts("package", context.getPackageName(), null));
181 }
182
183 /** Creates instances of Bubble. The default implementation just calls the constructor. */
184 @VisibleForTesting
185 public interface BubbleFactory {
186 NewBubble createBubble(@NonNull Context context, @NonNull Handler handler);
187 }
188
189 private static BubbleFactory bubbleFactory = NewBubble::new;
190
191 public static NewBubble createBubble(@NonNull Context context, @NonNull NewBubbleInfo info) {
192 NewBubble bubble = bubbleFactory.createBubble(context, new Handler());
193 bubble.setBubbleInfo(info);
194 return bubble;
195 }
196
197 @VisibleForTesting
198 public static void setBubbleFactory(@NonNull BubbleFactory bubbleFactory) {
199 NewBubble.bubbleFactory = bubbleFactory;
200 }
201
202 @VisibleForTesting
203 public static void resetBubbleFactory() {
204 NewBubble.bubbleFactory = NewBubble::new;
205 }
206
207 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
208 NewBubble(@NonNull Context context, @NonNull Handler handler) {
209 context = new ContextThemeWrapper(context, R.style.Theme_AppCompat);
210 this.context = context;
211 this.handler = handler;
212 windowManager = context.getSystemService(WindowManager.class);
213
214 viewHolder = new ViewHolder(context);
yueg81a77ff2017-12-05 10:29:03 -0800215
216 leftBoundary =
217 context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal)
218 - context
219 .getResources()
220 .getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal);
Eric Erfanian938468d2017-10-24 14:05:52 -0700221 }
222
223 /** Expands the main bubble menu. */
224 public void expand(boolean isUserAction) {
yueg81a77ff2017-12-05 10:29:03 -0800225 if (isUserAction) {
226 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_PRIMARY_BUTTON_EXPAND);
Eric Erfanian938468d2017-10-24 14:05:52 -0700227 }
yueg81a77ff2017-12-05 10:29:03 -0800228 viewHolder.setDrawerVisibility(View.INVISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700229 View expandedView = viewHolder.getExpandedView();
230 expandedView
231 .getViewTreeObserver()
232 .addOnPreDrawListener(
233 new OnPreDrawListener() {
234 @Override
235 public boolean onPreDraw() {
yueg81a77ff2017-12-05 10:29:03 -0800236 // Move the whole bubble up so that expanded view is still in screen
237 int moveUpDistance = viewHolder.getMoveUpDistance();
238 if (moveUpDistance != 0) {
239 savedYPosition = windowParams.y;
240 }
241
242 // Calculate the move-to-middle distance
243 int deltaX =
244 (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
245 float k = (float) moveUpDistance / deltaX;
246 if (isDrawingFromRight()) {
247 deltaX = -deltaX;
248 }
249
250 // Do X-move and Y-move together
251
252 final int startX = windowParams.x - deltaX;
253 final int startY = windowParams.y;
254 ValueAnimator animator = ValueAnimator.ofFloat(startX, windowParams.x);
255 animator.setInterpolator(new LinearOutSlowInInterpolator());
256 animator.addUpdateListener(
257 (valueAnimator) -> {
258 // Update windowParams and the root layout.
259 // We can't do ViewPropertyAnimation since it clips children.
260 float newX = (float) valueAnimator.getAnimatedValue();
261 if (moveUpDistance != 0) {
262 windowParams.y = startY - (int) (Math.abs(newX - (float) startX) * k);
263 }
264 windowParams.x = (int) newX;
265 windowManager.updateViewLayout(viewHolder.getRoot(), windowParams);
266 });
267 animator.addListener(
268 new AnimatorListener() {
269 @Override
270 public void onAnimationEnd(Animator animation) {
271 // Show expanded view
272 expandedView.setVisibility(View.VISIBLE);
273 expandedView.setTranslationY(-expandedView.getHeight());
274 expandedView
275 .animate()
276 .setInterpolator(new LinearOutSlowInInterpolator())
277 .translationY(0);
278 }
279
280 @Override
281 public void onAnimationStart(Animator animation) {}
282
283 @Override
284 public void onAnimationCancel(Animator animation) {}
285
286 @Override
287 public void onAnimationRepeat(Animator animation) {}
288 });
289 animator.start();
290
Eric Erfanian938468d2017-10-24 14:05:52 -0700291 expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
Eric Erfanian938468d2017-10-24 14:05:52 -0700292 return false;
293 }
294 });
295 setFocused(true);
296 expanded = true;
297 }
298
yueg81a77ff2017-12-05 10:29:03 -0800299 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
300 public void startCollapse(
301 @CollapseEnd int endAction, boolean isUserAction, boolean shouldRecoverYPosition) {
302 View expandedView = viewHolder.getExpandedView();
303 if (expandedView.getVisibility() != View.VISIBLE || collapseAnimation != null) {
304 // Drawer is already collapsed or animation is running.
305 return;
306 }
307
308 overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT;
309 setFocused(false);
310
311 if (collapseEndAction == CollapseEnd.NOTHING) {
312 collapseEndAction = endAction;
313 }
314 if (isUserAction && collapseEndAction == CollapseEnd.NOTHING) {
315 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_COLLAPSE_BY_USER);
316 }
317 // Animate expanded view to move from its position to above primary button and hide
318 collapseAnimation =
319 expandedView
320 .animate()
321 .translationY(-expandedView.getHeight())
322 .setInterpolator(new FastOutLinearInInterpolator())
323 .withEndAction(
324 () -> {
325 collapseAnimation = null;
326 expanded = false;
327
328 if (textShowing) {
329 // Will do resize once the text is done.
330 return;
331 }
332
333 // Set drawer visibility to INVISIBLE instead of GONE to keep primary button fixed
334 viewHolder.setDrawerVisibility(View.INVISIBLE);
335
336 // Do X-move and Y-move together
337 int deltaX =
338 (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
339 int startX = windowParams.x;
340 int startY = windowParams.y;
341 float k =
342 (savedYPosition != -1 && shouldRecoverYPosition)
343 ? (savedYPosition - startY) / (float) deltaX
344 : 0;
345 Path path = new Path();
346 path.moveTo(windowParams.x, windowParams.y);
347 path.lineTo(
348 windowParams.x - deltaX,
349 (savedYPosition != -1 && shouldRecoverYPosition)
350 ? savedYPosition
351 : windowParams.y);
352 // The position is not useful after collapse
353 savedYPosition = -1;
354
355 ValueAnimator animator = ValueAnimator.ofFloat(startX, startX - deltaX);
356 animator.setInterpolator(new LinearOutSlowInInterpolator());
357 animator.addUpdateListener(
358 (valueAnimator) -> {
359 // Update windowParams and the root layout.
360 // We can't do ViewPropertyAnimation since it clips children.
361 float newX = (float) valueAnimator.getAnimatedValue();
362 if (k != 0) {
363 windowParams.y = startY + (int) (Math.abs(newX - (float) startX) * k);
364 }
365 windowParams.x = (int) newX;
366 windowManager.updateViewLayout(viewHolder.getRoot(), windowParams);
367 });
368 animator.addListener(
369 new AnimatorListener() {
370 @Override
371 public void onAnimationEnd(Animator animation) {
372 // If collapse on the right side, the primary button move left a bit after
373 // drawer
374 // visibility becoming GONE. To avoid it, we create a new ViewHolder.
375 replaceViewHolder();
376 }
377
378 @Override
379 public void onAnimationStart(Animator animation) {}
380
381 @Override
382 public void onAnimationCancel(Animator animation) {}
383
384 @Override
385 public void onAnimationRepeat(Animator animation) {}
386 });
387 animator.start();
388
389 // If this collapse was to come before a hide, do it now.
390 if (collapseEndAction == CollapseEnd.HIDE) {
391 hide();
392 }
393 collapseEndAction = CollapseEnd.NOTHING;
394
395 // Resume normal gravity after any resizing is done.
396 handler.postDelayed(
397 () -> {
398 overrideGravity = null;
399 if (!viewHolder.isMoving()) {
400 viewHolder.undoGravityOverride();
401 }
402 },
403 // Need to wait twice as long for resize and layout
404 WINDOW_REDRAW_DELAY_MILLIS * 2);
405 });
406 }
407
Eric Erfanian938468d2017-10-24 14:05:52 -0700408 /**
409 * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is
410 * already showing this method does nothing.
411 */
412 public void show() {
413 if (collapseEndAction == CollapseEnd.HIDE) {
414 // If show() was called while collapsing, make sure we don't hide after.
415 collapseEndAction = CollapseEnd.NOTHING;
416 }
417 if (visibility == Visibility.SHOWING || visibility == Visibility.ENTERING) {
418 return;
419 }
420
421 hideAfterText = false;
422
423 if (windowParams == null) {
424 // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O.
425 @SuppressWarnings("deprecation")
426 @SuppressLint("InlinedApi")
427 int type =
428 BuildCompat.isAtLeastO()
429 ? LayoutParams.TYPE_APPLICATION_OVERLAY
430 : LayoutParams.TYPE_PHONE;
431
432 windowParams =
433 new LayoutParams(
434 type,
435 LayoutParams.FLAG_NOT_TOUCH_MODAL
436 | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
437 | LayoutParams.FLAG_NOT_FOCUSABLE
438 | LayoutParams.FLAG_LAYOUT_NO_LIMITS,
439 PixelFormat.TRANSLUCENT);
440 windowParams.gravity = Gravity.TOP | Gravity.LEFT;
yueg81a77ff2017-12-05 10:29:03 -0800441 windowParams.x = leftBoundary;
Eric Erfanian938468d2017-10-24 14:05:52 -0700442 windowParams.y = currentInfo.getStartingYPosition();
443 windowParams.height = LayoutParams.WRAP_CONTENT;
444 windowParams.width = LayoutParams.WRAP_CONTENT;
445 }
446
447 if (exitAnimator != null) {
448 exitAnimator.cancel();
449 exitAnimator = null;
450 } else {
451 windowManager.addView(viewHolder.getRoot(), windowParams);
452 viewHolder.getPrimaryButton().setScaleX(0);
453 viewHolder.getPrimaryButton().setScaleY(0);
454 }
455
456 viewHolder.setChildClickable(true);
457 visibility = Visibility.ENTERING;
458 viewHolder
459 .getPrimaryButton()
460 .animate()
461 .setInterpolator(new OvershootInterpolator())
462 .scaleX(1)
463 .scaleY(1)
464 .withEndAction(
465 () -> {
466 visibility = Visibility.SHOWING;
467 // Show the queued up text, if available.
468 if (textAfterShow != null) {
469 showText(textAfterShow);
470 textAfterShow = null;
471 }
472 })
473 .start();
474
475 updatePrimaryIconAnimation();
476 }
477
478 /** Hide the bubble. */
479 public void hide() {
480 if (hideAfterText) {
481 // hideAndReset() will be called after showing text, do nothing here.
482 return;
483 }
484 hideHelper(this::defaultAfterHidingAnimation);
485 }
486
487 /** Hide the bubble and reset {@viewHolder} to initial state */
488 public void hideAndReset() {
489 hideHelper(
490 () -> {
491 defaultAfterHidingAnimation();
492 reset();
493 });
494 }
495
496 /** Returns whether the bubble is currently visible */
497 public boolean isVisible() {
498 return visibility == Visibility.SHOWING
499 || visibility == Visibility.ENTERING
500 || visibility == Visibility.EXITING;
501 }
502
503 /**
504 * Set the info for this Bubble to display
505 *
506 * @param bubbleInfo the BubbleInfo to display in this Bubble.
507 */
508 public void setBubbleInfo(@NonNull NewBubbleInfo bubbleInfo) {
509 currentInfo = bubbleInfo;
510 update();
511 }
512
513 /**
514 * Update the state and behavior of actions.
515 *
516 * @param actions the new state of the bubble's actions
517 */
518 public void updateActions(@NonNull List<Action> actions) {
519 currentInfo = NewBubbleInfo.from(currentInfo).setActions(actions).build();
520 updateButtonStates();
521 }
522
yuega5a08d82017-10-31 14:11:53 -0700523 /**
524 * Update the avatar from photo.
525 *
526 * @param avatar the new photo avatar in the bubble's primary button
527 */
528 public void updatePhotoAvatar(@NonNull Drawable avatar) {
529 // Make it round
530 int bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
531 Drawable roundAvatar =
532 DrawableConverter.getRoundedDrawable(context, avatar, bubbleSize, bubbleSize);
533
534 updateAvatar(roundAvatar);
535 }
536
537 /**
538 * Update the avatar.
539 *
540 * @param avatar the new avatar in the bubble's primary button
541 */
542 public void updateAvatar(@NonNull Drawable avatar) {
543 if (!avatar.equals(currentInfo.getAvatar())) {
544 currentInfo = NewBubbleInfo.from(currentInfo).setAvatar(avatar).build();
545 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
546 }
547 }
548
Eric Erfanian938468d2017-10-24 14:05:52 -0700549 /** Returns the currently displayed NewBubbleInfo */
550 public NewBubbleInfo getBubbleInfo() {
551 return currentInfo;
552 }
553
554 /**
555 * Display text in the main bubble. The bubble's drawer is not expandable while text is showing,
556 * and the drawer will be closed if already open.
557 *
558 * @param text the text to display to the user
559 */
560 public void showText(@NonNull CharSequence text) {
561 textShowing = true;
562 if (expanded) {
yueg81a77ff2017-12-05 10:29:03 -0800563 startCollapse(
564 CollapseEnd.NOTHING, false /* isUserAction */, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700565 doShowText(text);
566 } else {
567 // Need to transition from old bounds to new bounds manually
568 NewChangeOnScreenBounds transition = new NewChangeOnScreenBounds();
569 // Prepare and capture start values
570 TransitionValues startValues = new TransitionValues();
571 startValues.view = viewHolder.getPrimaryButton();
572 transition.addTarget(startValues.view);
573 transition.captureStartValues(startValues);
574
575 // If our view is not laid out yet, postpone showing the text.
576 if (startValues.values.isEmpty()) {
577 textAfterShow = text;
578 return;
579 }
580
yueg81a77ff2017-12-05 10:29:03 -0800581 doShowText(text);
582 // Hide the text so we can animate it in
583 viewHolder.getPrimaryText().setAlpha(0);
Eric Erfanian938468d2017-10-24 14:05:52 -0700584
yueg81a77ff2017-12-05 10:29:03 -0800585 ViewAnimator primaryButton = viewHolder.getPrimaryButton();
586 // Cancel the automatic transition scheduled in doShowText
587 TransitionManager.endTransitions((ViewGroup) primaryButton.getParent());
588 primaryButton
589 .getViewTreeObserver()
590 .addOnPreDrawListener(
591 new OnPreDrawListener() {
592 @Override
593 public boolean onPreDraw() {
594 primaryButton.getViewTreeObserver().removeOnPreDrawListener(this);
Eric Erfanian938468d2017-10-24 14:05:52 -0700595
yueg81a77ff2017-12-05 10:29:03 -0800596 // Prepare and capture end values, always use the size of primaryText since
597 // its invisibility makes primaryButton smaller than expected
598 TransitionValues endValues = new TransitionValues();
599 endValues.values.put(
600 NewChangeOnScreenBounds.PROPNAME_WIDTH,
601 viewHolder.getPrimaryText().getWidth());
602 endValues.values.put(
603 NewChangeOnScreenBounds.PROPNAME_HEIGHT,
604 viewHolder.getPrimaryText().getHeight());
605 endValues.view = primaryButton;
606 transition.addTarget(endValues.view);
607 transition.captureEndValues(endValues);
Eric Erfanian938468d2017-10-24 14:05:52 -0700608
yueg81a77ff2017-12-05 10:29:03 -0800609 // animate the primary button bounds change
610 Animator bounds =
611 transition.createAnimator(primaryButton, startValues, endValues);
Eric Erfanian938468d2017-10-24 14:05:52 -0700612
yueg81a77ff2017-12-05 10:29:03 -0800613 // Animate the text in
614 Animator alpha =
615 ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f);
Eric Erfanian938468d2017-10-24 14:05:52 -0700616
yueg81a77ff2017-12-05 10:29:03 -0800617 AnimatorSet set = new AnimatorSet();
618 set.play(bounds).before(alpha);
619 set.start();
620 return false;
621 }
622 });
Eric Erfanian938468d2017-10-24 14:05:52 -0700623 }
624 handler.removeCallbacks(collapseRunnable);
625 handler.postDelayed(collapseRunnable, SHOW_TEXT_DURATION_MILLIS);
626 }
627
Eric Erfanian938468d2017-10-24 14:05:52 -0700628 @Nullable
629 Integer getGravityOverride() {
630 return overrideGravity;
631 }
632
633 void onMoveStart() {
yueg81a77ff2017-12-05 10:29:03 -0800634 if (viewHolder.getExpandedView().getVisibility() == View.VISIBLE) {
635 viewHolder.setDrawerVisibility(View.INVISIBLE);
636 }
637 expanded = false;
638 savedYPosition = -1;
639
Eric Erfanian938468d2017-10-24 14:05:52 -0700640 viewHolder
641 .getPrimaryButton()
642 .animate()
643 .translationZ(
yuegc6deafc2017-11-06 16:42:13 -0800644 context
645 .getResources()
646 .getDimensionPixelOffset(R.dimen.bubble_dragging_elevation_change));
Eric Erfanian938468d2017-10-24 14:05:52 -0700647 }
648
649 void onMoveFinish() {
650 viewHolder.getPrimaryButton().animate().translationZ(0);
Eric Erfanian938468d2017-10-24 14:05:52 -0700651 }
652
653 void primaryButtonClick() {
654 if (textShowing || currentInfo.getActions().isEmpty()) {
655 return;
656 }
657 if (expanded) {
yueg81a77ff2017-12-05 10:29:03 -0800658 startCollapse(
659 CollapseEnd.NOTHING, true /* isUserAction */, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700660 } else {
661 expand(true);
662 }
663 }
664
yueg81a77ff2017-12-05 10:29:03 -0800665 void onLeftRightSwitch(boolean onRight) {
666 // Set layout direction so the small icon is not partially hidden.
667 View primaryIcon = viewHolder.getPrimaryIcon();
668 int newGravity = (onRight ? Gravity.LEFT : Gravity.RIGHT) | Gravity.BOTTOM;
669 FrameLayout.LayoutParams layoutParams =
670 new FrameLayout.LayoutParams(primaryIcon.getWidth(), primaryIcon.getHeight(), newGravity);
671 primaryIcon.setLayoutParams(layoutParams);
672 }
673
Eric Erfanian938468d2017-10-24 14:05:52 -0700674 LayoutParams getWindowParams() {
675 return windowParams;
676 }
677
678 View getRootView() {
679 return viewHolder.getRoot();
680 }
681
682 /**
683 * Hide the bubble if visible. Will run a short exit animation and before hiding, and {@code
684 * afterHiding} after hiding. If the bubble is currently showing text, will hide after the text is
685 * done displaying. If the bubble is not visible this method does nothing.
686 */
687 private void hideHelper(Runnable afterHiding) {
688 if (visibility == Visibility.HIDDEN || visibility == Visibility.EXITING) {
689 return;
690 }
691
692 // Make bubble non clickable to prevent further buggy actions
693 viewHolder.setChildClickable(false);
694
695 if (textShowing) {
696 hideAfterText = true;
697 return;
698 }
699
700 if (collapseAnimation != null) {
701 collapseEndAction = CollapseEnd.HIDE;
702 return;
703 }
704
705 if (expanded) {
yueg81a77ff2017-12-05 10:29:03 -0800706 startCollapse(CollapseEnd.HIDE, false /* isUserAction */, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700707 return;
708 }
709
710 visibility = Visibility.EXITING;
711 exitAnimator =
712 viewHolder
713 .getPrimaryButton()
714 .animate()
715 .setInterpolator(new AnticipateInterpolator())
716 .scaleX(0)
717 .scaleY(0)
718 .withEndAction(afterHiding);
719 exitAnimator.start();
720 }
721
722 private void reset() {
723 viewHolder = new ViewHolder(viewHolder.getRoot().getContext());
724 update();
725 }
726
727 private void update() {
yuega5a08d82017-10-31 14:11:53 -0700728 // Whole primary button background
yueg84ac49b2017-11-01 16:22:28 -0700729 Drawable backgroundCirle =
730 context.getResources().getDrawable(R.drawable.bubble_shape_circle, context.getTheme());
Eric Erfanian938468d2017-10-24 14:05:52 -0700731 int primaryTint =
732 ColorUtils.compositeColors(
733 context.getColor(R.color.bubble_primary_background_darken),
734 currentInfo.getPrimaryColor());
yueg84ac49b2017-11-01 16:22:28 -0700735 backgroundCirle.mutate().setTint(primaryTint);
736 viewHolder.getPrimaryButton().setBackground(backgroundCirle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700737
yuega5a08d82017-10-31 14:11:53 -0700738 // Small icon
yueg84ac49b2017-11-01 16:22:28 -0700739 Drawable smallIconBackgroundCircle =
740 context
741 .getResources()
742 .getDrawable(R.drawable.bubble_shape_circle_small, context.getTheme());
743 smallIconBackgroundCircle.setTint(context.getColor(R.color.bubble_button_color_blue));
744 viewHolder.getPrimaryIcon().setBackground(smallIconBackgroundCircle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700745 viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon());
yuega5a08d82017-10-31 14:11:53 -0700746 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
Eric Erfanian938468d2017-10-24 14:05:52 -0700747
yuega5a08d82017-10-31 14:11:53 -0700748 updatePrimaryIconAnimation();
Eric Erfanian938468d2017-10-24 14:05:52 -0700749 updateButtonStates();
750 }
751
752 private void updatePrimaryIconAnimation() {
753 Drawable drawable = viewHolder.getPrimaryIcon().getDrawable();
754 if (drawable instanceof Animatable) {
755 if (isVisible()) {
756 ((Animatable) drawable).start();
757 } else {
758 ((Animatable) drawable).stop();
759 }
760 }
761 }
762
763 private void updateButtonStates() {
yueg84ac49b2017-11-01 16:22:28 -0700764 configureButton(currentInfo.getActions().get(0), viewHolder.getFullScreenButton());
765 configureButton(currentInfo.getActions().get(1), viewHolder.getMuteButton());
766 configureButton(currentInfo.getActions().get(2), viewHolder.getAudioRouteButton());
767 configureButton(currentInfo.getActions().get(3), viewHolder.getEndCallButton());
Eric Erfanian938468d2017-10-24 14:05:52 -0700768 }
769
770 private void doShowText(@NonNull CharSequence text) {
771 TransitionManager.beginDelayedTransition((ViewGroup) viewHolder.getPrimaryButton().getParent());
772 viewHolder.getPrimaryText().setText(text);
773 viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_TEXT);
774 }
775
yueg84ac49b2017-11-01 16:22:28 -0700776 private void configureButton(Action action, NewCheckableButton button) {
777 button.setCompoundDrawablesWithIntrinsicBounds(action.getIconDrawable(), null, null, null);
Eric Erfanian938468d2017-10-24 14:05:52 -0700778 button.setChecked(action.isChecked());
779 button.setEnabled(action.isEnabled());
yueg84ac49b2017-11-01 16:22:28 -0700780 button.setText(action.getName());
Eric Erfanian938468d2017-10-24 14:05:52 -0700781 button.setOnClickListener(v -> doAction(action));
782 }
783
784 private void doAction(Action action) {
785 try {
786 action.getIntent().send();
787 } catch (CanceledException e) {
788 throw new RuntimeException(e);
789 }
790 }
791
yueg81a77ff2017-12-05 10:29:03 -0800792 /**
793 * Create a new ViewHolder object to replace the old one.It only happens when not moving and
794 * collapsed.
795 */
796 void replaceViewHolder() {
797 LogUtil.enterBlock("NewBubble.replaceViewHolder");
Eric Erfanian938468d2017-10-24 14:05:52 -0700798 ViewHolder oldViewHolder = viewHolder;
Eric Erfanian938468d2017-10-24 14:05:52 -0700799
yueg81a77ff2017-12-05 10:29:03 -0800800 // Create a new ViewHolder and copy needed info.
801 viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext());
802 viewHolder
803 .getPrimaryButton()
804 .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild());
805 viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText());
Eric Erfanian938468d2017-10-24 14:05:52 -0700806
yueg81a77ff2017-12-05 10:29:03 -0800807 int size = context.getResources().getDimensionPixelSize(R.dimen.bubble_small_icon_size);
808 viewHolder
809 .getPrimaryIcon()
810 .setLayoutParams(
811 new FrameLayout.LayoutParams(
812 size,
813 size,
814 Gravity.BOTTOM | (isDrawingFromRight() ? Gravity.LEFT : Gravity.RIGHT)));
Eric Erfanian938468d2017-10-24 14:05:52 -0700815
yueg81a77ff2017-12-05 10:29:03 -0800816 update();
817
818 // Add new view at its horizontal boundary
Eric Erfanian938468d2017-10-24 14:05:52 -0700819 ViewGroup root = viewHolder.getRoot();
yueg81a77ff2017-12-05 10:29:03 -0800820 windowParams.x = leftBoundary;
821 windowParams.gravity = Gravity.TOP | (isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT);
Eric Erfanian938468d2017-10-24 14:05:52 -0700822 windowManager.addView(root, windowParams);
yueg81a77ff2017-12-05 10:29:03 -0800823
824 // Remove the old view after delay
Eric Erfanian938468d2017-10-24 14:05:52 -0700825 root.getViewTreeObserver()
826 .addOnPreDrawListener(
827 new OnPreDrawListener() {
828 @Override
829 public boolean onPreDraw() {
830 root.getViewTreeObserver().removeOnPreDrawListener(this);
831 // Wait a bit before removing the old view; make sure the new one has drawn over it.
832 handler.postDelayed(
833 () -> windowManager.removeView(oldViewHolder.getRoot()),
834 WINDOW_REDRAW_DELAY_MILLIS);
835 return true;
836 }
837 });
838 }
839
yueg81a77ff2017-12-05 10:29:03 -0800840 int getDrawerVisibility() {
841 return viewHolder.getExpandedView().getVisibility();
Eric Erfanian938468d2017-10-24 14:05:52 -0700842 }
843
844 private boolean isDrawingFromRight() {
845 return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
846 }
847
848 private void setFocused(boolean focused) {
849 if (focused) {
850 windowParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
851 } else {
852 windowParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
853 }
854 windowManager.updateViewLayout(getRootView(), windowParams);
855 }
856
857 private void defaultAfterHidingAnimation() {
858 exitAnimator = null;
859 windowManager.removeView(viewHolder.getRoot());
860 visibility = Visibility.HIDDEN;
861
862 updatePrimaryIconAnimation();
863 }
864
yueg81a77ff2017-12-05 10:29:03 -0800865 private void logBasicOrCallImpression(DialerImpression.Type impressionType) {
866 DialerCall call = CallList.getInstance().getActiveOrBackgroundCall();
867 if (call != null) {
868 Logger.get(context)
869 .logCallImpression(impressionType, call.getUniqueCallId(), call.getTimeAddedMs());
870 } else {
871 Logger.get(context).logImpression(impressionType);
872 }
873 }
874
Eric Erfanian938468d2017-10-24 14:05:52 -0700875 @VisibleForTesting
876 class ViewHolder {
877
yuega5a08d82017-10-31 14:11:53 -0700878 public static final int CHILD_INDEX_AVATAR_AND_ICON = 0;
Eric Erfanian938468d2017-10-24 14:05:52 -0700879 public static final int CHILD_INDEX_TEXT = 1;
880
881 private final NewMoveHandler moveHandler;
882 private final NewWindowRoot root;
883 private final ViewAnimator primaryButton;
884 private final ImageView primaryIcon;
yuega5a08d82017-10-31 14:11:53 -0700885 private final ImageView primaryAvatar;
Eric Erfanian938468d2017-10-24 14:05:52 -0700886 private final TextView primaryText;
887
888 private final NewCheckableButton fullScreenButton;
889 private final NewCheckableButton muteButton;
890 private final NewCheckableButton audioRouteButton;
891 private final NewCheckableButton endCallButton;
892 private final View expandedView;
893
894 public ViewHolder(Context context) {
895 // Window root is not in the layout file so that the inflater has a view to inflate into
896 this.root = new NewWindowRoot(context);
897 LayoutInflater inflater = LayoutInflater.from(root.getContext());
898 View contentView = inflater.inflate(R.layout.new_bubble_base, root, true);
899 expandedView = contentView.findViewById(R.id.bubble_expanded_layout);
900 primaryButton = contentView.findViewById(R.id.bubble_button_primary);
yuega5a08d82017-10-31 14:11:53 -0700901 primaryAvatar = contentView.findViewById(R.id.bubble_icon_avatar);
Eric Erfanian938468d2017-10-24 14:05:52 -0700902 primaryIcon = contentView.findViewById(R.id.bubble_icon_primary);
903 primaryText = contentView.findViewById(R.id.bubble_text);
904
905 fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen);
906 muteButton = contentView.findViewById(R.id.bubble_button_mute);
907 audioRouteButton = contentView.findViewById(R.id.bubble_button_audio_route);
908 endCallButton = contentView.findViewById(R.id.bubble_button_end_call);
909
910 root.setOnBackPressedListener(
911 () -> {
912 if (visibility == Visibility.SHOWING && expanded) {
yueg81a77ff2017-12-05 10:29:03 -0800913 startCollapse(
914 CollapseEnd.NOTHING, true /* isUserAction */, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700915 return true;
916 }
917 return false;
918 });
919 root.setOnConfigurationChangedListener(
920 (configuration) -> {
921 // The values in the current MoveHandler may be stale, so replace it. Then ensure the
922 // Window is in bounds
923 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
924 moveHandler.snapToBounds();
925 });
926 root.setOnTouchListener(
927 (v, event) -> {
928 if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
yueg81a77ff2017-12-05 10:29:03 -0800929 startCollapse(
930 CollapseEnd.NOTHING, true /* isUserAction */, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700931 return true;
932 }
933 return false;
934 });
935 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
936 }
937
938 private void setChildClickable(boolean clickable) {
939 fullScreenButton.setClickable(clickable);
940 muteButton.setClickable(clickable);
941 audioRouteButton.setClickable(clickable);
942 endCallButton.setClickable(clickable);
943
944 // For primaryButton
945 moveHandler.setClickable(clickable);
946 }
947
yueg81a77ff2017-12-05 10:29:03 -0800948 public int getMoveUpDistance() {
949 int deltaAllowed =
950 expandedView.getHeight()
951 - context
952 .getResources()
953 .getDimensionPixelOffset(R.dimen.bubble_button_padding_vertical)
954 * 2;
955 return moveHandler.getMoveUpDistance(deltaAllowed);
956 }
957
Eric Erfanian938468d2017-10-24 14:05:52 -0700958 public ViewGroup getRoot() {
959 return root;
960 }
961
962 public ViewAnimator getPrimaryButton() {
963 return primaryButton;
964 }
965
966 public ImageView getPrimaryIcon() {
967 return primaryIcon;
968 }
969
yuega5a08d82017-10-31 14:11:53 -0700970 public ImageView getPrimaryAvatar() {
971 return primaryAvatar;
972 }
973
Eric Erfanian938468d2017-10-24 14:05:52 -0700974 public TextView getPrimaryText() {
975 return primaryText;
976 }
977
978 public View getExpandedView() {
979 return expandedView;
980 }
981
982 public NewCheckableButton getFullScreenButton() {
983 return fullScreenButton;
984 }
985
986 public NewCheckableButton getMuteButton() {
987 return muteButton;
988 }
989
990 public NewCheckableButton getAudioRouteButton() {
991 return audioRouteButton;
992 }
993
994 public NewCheckableButton getEndCallButton() {
995 return endCallButton;
996 }
997
998 public void setDrawerVisibility(int visibility) {
999 expandedView.setVisibility(visibility);
1000 }
1001
1002 public boolean isMoving() {
1003 return moveHandler.isMoving();
1004 }
1005
1006 public void undoGravityOverride() {
1007 moveHandler.undoGravityOverride();
1008 }
1009 }
Eric Erfanian938468d2017-10-24 14:05:52 -07001010}