blob: 54e56ba61fd99b2b122d18d4ee06cc039289165b [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;
Eric Erfanian938468d2017-10-24 14:05:52 -070039import android.support.v4.os.BuildCompat;
Eric Erfanian938468d2017-10-24 14:05:52 -070040import android.support.v4.view.animation.LinearOutSlowInInterpolator;
yueg07e323c2017-12-19 16:05:47 -080041import android.text.TextUtils;
Eric Erfanian938468d2017-10-24 14:05:52 -070042import android.view.ContextThemeWrapper;
43import android.view.Gravity;
44import android.view.LayoutInflater;
45import android.view.MotionEvent;
46import android.view.View;
yuega235e132017-12-13 14:13:57 -080047import android.view.View.AccessibilityDelegate;
Eric Erfanian938468d2017-10-24 14:05:52 -070048import android.view.ViewGroup;
Eric Erfanian938468d2017-10-24 14:05:52 -070049import android.view.ViewTreeObserver.OnPreDrawListener;
50import android.view.WindowManager;
51import android.view.WindowManager.LayoutParams;
yuega235e132017-12-13 14:13:57 -080052import android.view.accessibility.AccessibilityNodeInfo;
53import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
yueg0b4755c2017-12-18 10:01:03 -080054import android.view.animation.AccelerateDecelerateInterpolator;
Eric Erfanian938468d2017-10-24 14:05:52 -070055import android.view.animation.AnticipateInterpolator;
56import android.view.animation.OvershootInterpolator;
57import android.widget.ImageView;
yuegf473e1d2018-01-02 16:23:14 -080058import android.widget.Toast;
yueg81a77ff2017-12-05 10:29:03 -080059import com.android.dialer.common.LogUtil;
60import com.android.dialer.logging.DialerImpression;
61import com.android.dialer.logging.Logger;
yuega5a08d82017-10-31 14:11:53 -070062import com.android.dialer.util.DrawableConverter;
yueg81a77ff2017-12-05 10:29:03 -080063import com.android.incallui.call.CallList;
64import com.android.incallui.call.DialerCall;
Eric Erfanian938468d2017-10-24 14:05:52 -070065import com.android.newbubble.NewBubbleInfo.Action;
66import java.lang.annotation.Retention;
67import java.lang.annotation.RetentionPolicy;
68import java.util.List;
yueg07e323c2017-12-19 16:05:47 -080069import java.util.Locale;
Eric Erfanian938468d2017-10-24 14:05:52 -070070
71/**
72 * Creates and manages a bubble window from information in a {@link NewBubbleInfo}. Before creating,
73 * be sure to check whether bubbles may be shown using {@link #canShowBubbles(Context)} and request
74 * permission if necessary ({@link #getRequestPermissionIntent(Context)} is provided for
75 * convenience)
76 */
77public class NewBubble {
78 // This class has some odd behavior that is not immediately obvious in order to avoid jank when
79 // resizing. See http://go/bubble-resize for details.
80
Eric Erfanian938468d2017-10-24 14:05:52 -070081 // How long the new window should show before destroying the old one during resize operations.
82 // This ensures the new window has had time to draw first.
83 private static final int WINDOW_REDRAW_DELAY_MILLIS = 50;
84
yueg0b4755c2017-12-18 10:01:03 -080085 private static final int EXPAND_AND_COLLAPSE_ANIMATION_DURATION = 200;
yueg6518fdb2018-01-09 17:30:36 -080086 private static final int HIDE_BUBBLE_ANIMATION_DURATION = 250;
yueg0b4755c2017-12-18 10:01:03 -080087
Eric Erfanian938468d2017-10-24 14:05:52 -070088 private static Boolean canShowBubblesForTesting = null;
89
yueg0b4755c2017-12-18 10:01:03 -080090 private final AccelerateDecelerateInterpolator accelerateDecelerateInterpolator =
91 new AccelerateDecelerateInterpolator();
92
Eric Erfanian938468d2017-10-24 14:05:52 -070093 private final Context context;
94 private final WindowManager windowManager;
95
96 private final Handler handler;
97 private LayoutParams windowParams;
98
99 // Initialized in factory method
100 @SuppressWarnings("NullableProblems")
101 @NonNull
102 private NewBubbleInfo currentInfo;
103
yueg349ac602018-01-17 09:02:45 -0800104 @VisibleForTesting @Visibility int visibility;
Eric Erfanian938468d2017-10-24 14:05:52 -0700105 private boolean expanded;
Eric Erfanian938468d2017-10-24 14:05:52 -0700106 private CharSequence textAfterShow;
107 private int collapseEndAction;
108
yueg81a77ff2017-12-05 10:29:03 -0800109 ViewHolder viewHolder;
yueg6518fdb2018-01-09 17:30:36 -0800110 private AnimatorSet collapseAnimatorSet;
Eric Erfanian938468d2017-10-24 14:05:52 -0700111 private Integer overrideGravity;
yueg87111362017-12-08 12:45:50 -0800112 @VisibleForTesting AnimatorSet exitAnimatorSet;
yueg349ac602018-01-17 09:02:45 -0800113 @VisibleForTesting AnimatorSet enterAnimatorSet;
Eric Erfanian938468d2017-10-24 14:05:52 -0700114
yuegbf7e8852018-01-23 10:00:15 -0800115 private int primaryIconMoveDistance;
yueg87111362017-12-08 12:45:50 -0800116 private final int leftBoundary;
yueg81a77ff2017-12-05 10:29:03 -0800117 private int savedYPosition = -1;
118
Eric Erfanian938468d2017-10-24 14:05:52 -0700119 /** Type of action after bubble collapse */
120 @Retention(RetentionPolicy.SOURCE)
121 @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE})
yueg349ac602018-01-17 09:02:45 -0800122 private @interface CollapseEnd {
Eric Erfanian938468d2017-10-24 14:05:52 -0700123 int NOTHING = 0;
124 int HIDE = 1;
125 }
126
127 @Retention(RetentionPolicy.SOURCE)
yueg349ac602018-01-17 09:02:45 -0800128 @VisibleForTesting
Eric Erfanian938468d2017-10-24 14:05:52 -0700129 @IntDef({Visibility.ENTERING, Visibility.SHOWING, Visibility.EXITING, Visibility.HIDDEN})
yueg349ac602018-01-17 09:02:45 -0800130 @interface Visibility {
Eric Erfanian938468d2017-10-24 14:05:52 -0700131 int HIDDEN = 0;
132 int ENTERING = 1;
133 int SHOWING = 2;
134 int EXITING = 3;
135 }
136
137 /** Indicate bubble expansion state. */
138 @Retention(RetentionPolicy.SOURCE)
139 @IntDef({ExpansionState.START_EXPANDING, ExpansionState.START_COLLAPSING})
140 public @interface ExpansionState {
141 // TODO(yueg): add more states when needed
142 int START_EXPANDING = 0;
143 int START_COLLAPSING = 1;
144 }
145
146 /**
147 * Determines whether bubbles can be shown based on permissions obtained. This should be checked
148 * before attempting to create a Bubble.
149 *
150 * @return true iff bubbles are able to be shown.
151 * @see Settings#canDrawOverlays(Context)
152 */
153 public static boolean canShowBubbles(@NonNull Context context) {
154 return canShowBubblesForTesting != null
155 ? canShowBubblesForTesting
156 : Settings.canDrawOverlays(context);
157 }
158
159 @VisibleForTesting(otherwise = VisibleForTesting.NONE)
160 public static void setCanShowBubblesForTesting(boolean canShowBubbles) {
161 canShowBubblesForTesting = canShowBubbles;
162 }
163
164 /** Returns an Intent to request permission to show overlays */
165 @NonNull
166 public static Intent getRequestPermissionIntent(@NonNull Context context) {
167 return new Intent(
168 Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
169 Uri.fromParts("package", context.getPackageName(), null));
170 }
171
172 /** Creates instances of Bubble. The default implementation just calls the constructor. */
173 @VisibleForTesting
174 public interface BubbleFactory {
175 NewBubble createBubble(@NonNull Context context, @NonNull Handler handler);
176 }
177
178 private static BubbleFactory bubbleFactory = NewBubble::new;
179
180 public static NewBubble createBubble(@NonNull Context context, @NonNull NewBubbleInfo info) {
181 NewBubble bubble = bubbleFactory.createBubble(context, new Handler());
182 bubble.setBubbleInfo(info);
183 return bubble;
184 }
185
186 @VisibleForTesting
187 public static void setBubbleFactory(@NonNull BubbleFactory bubbleFactory) {
188 NewBubble.bubbleFactory = bubbleFactory;
189 }
190
191 @VisibleForTesting
192 public static void resetBubbleFactory() {
193 NewBubble.bubbleFactory = NewBubble::new;
194 }
195
196 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
197 NewBubble(@NonNull Context context, @NonNull Handler handler) {
198 context = new ContextThemeWrapper(context, R.style.Theme_AppCompat);
199 this.context = context;
200 this.handler = handler;
201 windowManager = context.getSystemService(WindowManager.class);
202
203 viewHolder = new ViewHolder(context);
yueg81a77ff2017-12-05 10:29:03 -0800204
205 leftBoundary =
206 context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal)
207 - context
208 .getResources()
209 .getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal);
yueg87111362017-12-08 12:45:50 -0800210 primaryIconMoveDistance =
211 context.getResources().getDimensionPixelSize(R.dimen.bubble_size)
212 - context.getResources().getDimensionPixelSize(R.dimen.bubble_small_icon_size);
Eric Erfanian938468d2017-10-24 14:05:52 -0700213 }
214
215 /** Expands the main bubble menu. */
yuegf539f782017-12-18 16:20:58 -0800216 public void expand() {
yuega235e132017-12-13 14:13:57 -0800217 setPrimaryButtonAccessibilityAction(
218 context.getString(R.string.a11y_bubble_primary_button_collapse_action));
yueg0b4755c2017-12-18 10:01:03 -0800219
yueg81a77ff2017-12-05 10:29:03 -0800220 viewHolder.setDrawerVisibility(View.INVISIBLE);
yueg0b4755c2017-12-18 10:01:03 -0800221 viewHolder.getArrow().setVisibility(View.INVISIBLE);
222 // No click during animation to avoid jank.
223 viewHolder.setPrimaryButtonClickable(false);
224
Eric Erfanian938468d2017-10-24 14:05:52 -0700225 View expandedView = viewHolder.getExpandedView();
226 expandedView
227 .getViewTreeObserver()
228 .addOnPreDrawListener(
229 new OnPreDrawListener() {
230 @Override
231 public boolean onPreDraw() {
yueg81a77ff2017-12-05 10:29:03 -0800232 // Move the whole bubble up so that expanded view is still in screen
233 int moveUpDistance = viewHolder.getMoveUpDistance();
234 if (moveUpDistance != 0) {
235 savedYPosition = windowParams.y;
236 }
237
yueg0b4755c2017-12-18 10:01:03 -0800238 // Animation 1: animate x-move and y-move (if needed) together
yueg81a77ff2017-12-05 10:29:03 -0800239 int deltaX =
240 (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
yueg0b4755c2017-12-18 10:01:03 -0800241 float k = -(float) moveUpDistance / deltaX;
yueg81a77ff2017-12-05 10:29:03 -0800242 if (isDrawingFromRight()) {
243 deltaX = -deltaX;
244 }
yueg0b4755c2017-12-18 10:01:03 -0800245 ValueAnimator xValueAnimator =
246 createBubbleMoveAnimator(
247 windowParams.x - deltaX, windowParams.x, windowParams.y, k);
yueg81a77ff2017-12-05 10:29:03 -0800248
yueg0b4755c2017-12-18 10:01:03 -0800249 // Show expanded view
250 expandedView.setVisibility(View.VISIBLE);
yueg81a77ff2017-12-05 10:29:03 -0800251
yueg0b4755c2017-12-18 10:01:03 -0800252 // Animator 2: reveal expanded view from top left or top right
253 View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
254 ValueAnimator revealAnim =
255 createOpenCloseOutlineProvider(expandedMenu)
256 .createRevealAnimator(expandedMenu, false);
257 revealAnim.setInterpolator(accelerateDecelerateInterpolator);
258
259 // Animator 3: expanded view fade in
260 Animator fadeIn = ObjectAnimator.ofFloat(expandedView, "alpha", 0, 1);
261 fadeIn.setInterpolator(accelerateDecelerateInterpolator);
262
263 // Play all animation together
264 AnimatorSet expandAnimatorSet = new AnimatorSet();
265 expandAnimatorSet.playTogether(revealAnim, fadeIn, xValueAnimator);
266 expandAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
267 expandAnimatorSet.addListener(
yueg87111362017-12-08 12:45:50 -0800268 new AnimatorListenerAdapter() {
yueg81a77ff2017-12-05 10:29:03 -0800269 @Override
270 public void onAnimationEnd(Animator animation) {
yueg0b4755c2017-12-18 10:01:03 -0800271 // Show arrow after animation
272 viewHolder.getArrow().setVisibility(View.VISIBLE);
273 // Safe to click primary button now
274 viewHolder.setPrimaryButtonClickable(true);
yueg81a77ff2017-12-05 10:29:03 -0800275 }
yueg81a77ff2017-12-05 10:29:03 -0800276 });
yueg0b4755c2017-12-18 10:01:03 -0800277 expandAnimatorSet.start();
yueg81a77ff2017-12-05 10:29:03 -0800278
Eric Erfanian938468d2017-10-24 14:05:52 -0700279 expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
Eric Erfanian938468d2017-10-24 14:05:52 -0700280 return false;
281 }
282 });
283 setFocused(true);
284 expanded = true;
285 }
286
yueg81a77ff2017-12-05 10:29:03 -0800287 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
yuegf539f782017-12-18 16:20:58 -0800288 public void startCollapse(@CollapseEnd int endAction, boolean shouldRecoverYPosition) {
yueg81a77ff2017-12-05 10:29:03 -0800289 View expandedView = viewHolder.getExpandedView();
yueg6518fdb2018-01-09 17:30:36 -0800290 if (expandedView.getVisibility() != View.VISIBLE || collapseAnimatorSet != null) {
yueg81a77ff2017-12-05 10:29:03 -0800291 // Drawer is already collapsed or animation is running.
292 return;
293 }
294
295 overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT;
296 setFocused(false);
297
298 if (collapseEndAction == CollapseEnd.NOTHING) {
299 collapseEndAction = endAction;
300 }
yuega235e132017-12-13 14:13:57 -0800301 setPrimaryButtonAccessibilityAction(
302 context.getString(R.string.a11y_bubble_primary_button_expand_action));
yueg0b4755c2017-12-18 10:01:03 -0800303
304 // Hide arrow before animation
305 viewHolder.getArrow().setVisibility(View.INVISIBLE);
306
307 // No click during animation to avoid jank.
308 viewHolder.setPrimaryButtonClickable(false);
309
310 // Calculate animation values
311 int deltaX = (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
312 float k =
313 (savedYPosition != -1 && shouldRecoverYPosition)
314 ? (savedYPosition - windowParams.y) / (float) deltaX
315 : 0;
316 // The position is not useful after collapse
317 savedYPosition = -1;
318
319 // Animation 1: animate x-move and y-move (if needed) together
320 ValueAnimator xValueAnimator =
321 createBubbleMoveAnimator(windowParams.x, windowParams.x - deltaX, windowParams.y, k);
322
323 // Animator 2: hide expanded view to top left or top right
324 View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
325 ValueAnimator revealAnim =
326 createOpenCloseOutlineProvider(expandedMenu).createRevealAnimator(expandedMenu, true);
327 revealAnim.setInterpolator(accelerateDecelerateInterpolator);
328
329 // Animator 3: expanded view fade out
330 Animator fadeOut = ObjectAnimator.ofFloat(expandedView, "alpha", 1, 0);
331 fadeOut.setInterpolator(accelerateDecelerateInterpolator);
332
333 // Play all animation together
yueg6518fdb2018-01-09 17:30:36 -0800334 collapseAnimatorSet = new AnimatorSet();
yueg0b4755c2017-12-18 10:01:03 -0800335 collapseAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
336 collapseAnimatorSet.playTogether(revealAnim, fadeOut, xValueAnimator);
337 collapseAnimatorSet.addListener(
338 new AnimatorListenerAdapter() {
339 @Override
340 public void onAnimationEnd(Animator animation) {
yueg6518fdb2018-01-09 17:30:36 -0800341 collapseAnimatorSet = null;
yueg0b4755c2017-12-18 10:01:03 -0800342 expanded = false;
343
yueg0b4755c2017-12-18 10:01:03 -0800344 // If collapse on the right side, the primary button move left a bit after drawer
345 // visibility becoming GONE. To avoid it, we create a new ViewHolder.
346 // It also set primary button clickable back to true, so no need to reset manually.
347 replaceViewHolder();
348
yueg6518fdb2018-01-09 17:30:36 -0800349 // If this collapse was to come before a hide, do it now.
350 if (collapseEndAction == CollapseEnd.HIDE) {
351 hide();
352 collapseEndAction = CollapseEnd.NOTHING;
353 }
354
yueg0b4755c2017-12-18 10:01:03 -0800355 // Resume normal gravity after any resizing is done.
356 handler.postDelayed(
yueg81a77ff2017-12-05 10:29:03 -0800357 () -> {
yueg0b4755c2017-12-18 10:01:03 -0800358 overrideGravity = null;
359 if (!viewHolder.isMoving()) {
360 viewHolder.undoGravityOverride();
yueg81a77ff2017-12-05 10:29:03 -0800361 }
yueg0b4755c2017-12-18 10:01:03 -0800362 },
363 // Need to wait twice as long for resize and layout
364 WINDOW_REDRAW_DELAY_MILLIS * 2);
365 }
366 });
367 collapseAnimatorSet.start();
yueg81a77ff2017-12-05 10:29:03 -0800368 }
369
Eric Erfanian938468d2017-10-24 14:05:52 -0700370 /**
371 * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is
372 * already showing this method does nothing.
373 */
374 public void show() {
375 if (collapseEndAction == CollapseEnd.HIDE) {
376 // If show() was called while collapsing, make sure we don't hide after.
377 collapseEndAction = CollapseEnd.NOTHING;
378 }
379 if (visibility == Visibility.SHOWING || visibility == Visibility.ENTERING) {
380 return;
381 }
382
yueg2f664032018-01-10 11:30:30 -0800383 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_SHOW);
384
yueg07e323c2017-12-19 16:05:47 -0800385 boolean isRtl =
386 TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
Eric Erfanian938468d2017-10-24 14:05:52 -0700387 if (windowParams == null) {
388 // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O.
389 @SuppressWarnings("deprecation")
390 @SuppressLint("InlinedApi")
391 int type =
392 BuildCompat.isAtLeastO()
393 ? LayoutParams.TYPE_APPLICATION_OVERLAY
394 : LayoutParams.TYPE_PHONE;
395
396 windowParams =
397 new LayoutParams(
398 type,
399 LayoutParams.FLAG_NOT_TOUCH_MODAL
400 | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
401 | LayoutParams.FLAG_NOT_FOCUSABLE
402 | LayoutParams.FLAG_LAYOUT_NO_LIMITS,
403 PixelFormat.TRANSLUCENT);
yueg07e323c2017-12-19 16:05:47 -0800404 windowParams.gravity = Gravity.TOP | (isRtl ? Gravity.RIGHT : Gravity.LEFT);
yueg81a77ff2017-12-05 10:29:03 -0800405 windowParams.x = leftBoundary;
Eric Erfanian938468d2017-10-24 14:05:52 -0700406 windowParams.y = currentInfo.getStartingYPosition();
407 windowParams.height = LayoutParams.WRAP_CONTENT;
408 windowParams.width = LayoutParams.WRAP_CONTENT;
409 }
410
yueg87111362017-12-08 12:45:50 -0800411 if (exitAnimatorSet != null) {
412 exitAnimatorSet.removeAllListeners();
413 exitAnimatorSet.cancel();
414 exitAnimatorSet = null;
Eric Erfanian938468d2017-10-24 14:05:52 -0700415 } else {
416 windowManager.addView(viewHolder.getRoot(), windowParams);
yueg87111362017-12-08 12:45:50 -0800417 viewHolder.getPrimaryButton().setVisibility(View.VISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700418 viewHolder.getPrimaryButton().setScaleX(0);
419 viewHolder.getPrimaryButton().setScaleY(0);
yueg87111362017-12-08 12:45:50 -0800420 viewHolder.getPrimaryAvatar().setAlpha(0f);
421 viewHolder.getPrimaryIcon().setAlpha(0f);
yueg07e323c2017-12-19 16:05:47 -0800422 if (isRtl) {
423 onLeftRightSwitch(true);
424 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700425 }
426
427 viewHolder.setChildClickable(true);
428 visibility = Visibility.ENTERING;
yueg87111362017-12-08 12:45:50 -0800429
yuega235e132017-12-13 14:13:57 -0800430 setPrimaryButtonAccessibilityAction(
431 context.getString(R.string.a11y_bubble_primary_button_expand_action));
432
yueg87111362017-12-08 12:45:50 -0800433 // Show bubble animation: scale the whole bubble to 1, and change avatar+icon's alpha to 1
434 ObjectAnimator scaleXAnimator =
435 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 1);
436 ObjectAnimator scaleYAnimator =
437 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 1);
438 ObjectAnimator avatarAlphaAnimator =
439 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 1);
440 ObjectAnimator iconAlphaAnimator =
441 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 1);
yueg349ac602018-01-17 09:02:45 -0800442 enterAnimatorSet = new AnimatorSet();
yueg87111362017-12-08 12:45:50 -0800443 enterAnimatorSet.playTogether(
444 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
445 enterAnimatorSet.setInterpolator(new OvershootInterpolator());
446 enterAnimatorSet.addListener(
447 new AnimatorListenerAdapter() {
448 @Override
449 public void onAnimationEnd(Animator animation) {
450 visibility = Visibility.SHOWING;
451 // Show the queued up text, if available.
452 if (textAfterShow != null) {
453 showText(textAfterShow);
454 textAfterShow = null;
455 }
456 }
457 });
458 enterAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700459
460 updatePrimaryIconAnimation();
461 }
462
463 /** Hide the bubble. */
464 public void hide() {
Eric Erfanian938468d2017-10-24 14:05:52 -0700465 hideHelper(this::defaultAfterHidingAnimation);
466 }
467
468 /** Hide the bubble and reset {@viewHolder} to initial state */
469 public void hideAndReset() {
470 hideHelper(
471 () -> {
472 defaultAfterHidingAnimation();
473 reset();
474 });
475 }
476
477 /** Returns whether the bubble is currently visible */
478 public boolean isVisible() {
479 return visibility == Visibility.SHOWING
480 || visibility == Visibility.ENTERING
481 || visibility == Visibility.EXITING;
482 }
483
484 /**
485 * Set the info for this Bubble to display
486 *
487 * @param bubbleInfo the BubbleInfo to display in this Bubble.
488 */
489 public void setBubbleInfo(@NonNull NewBubbleInfo bubbleInfo) {
490 currentInfo = bubbleInfo;
491 update();
492 }
493
494 /**
495 * Update the state and behavior of actions.
496 *
497 * @param actions the new state of the bubble's actions
498 */
499 public void updateActions(@NonNull List<Action> actions) {
500 currentInfo = NewBubbleInfo.from(currentInfo).setActions(actions).build();
501 updateButtonStates();
502 }
503
yuega5a08d82017-10-31 14:11:53 -0700504 /**
505 * Update the avatar from photo.
506 *
507 * @param avatar the new photo avatar in the bubble's primary button
508 */
509 public void updatePhotoAvatar(@NonNull Drawable avatar) {
510 // Make it round
511 int bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
512 Drawable roundAvatar =
513 DrawableConverter.getRoundedDrawable(context, avatar, bubbleSize, bubbleSize);
514
515 updateAvatar(roundAvatar);
516 }
517
518 /**
519 * Update the avatar.
520 *
521 * @param avatar the new avatar in the bubble's primary button
522 */
523 public void updateAvatar(@NonNull Drawable avatar) {
524 if (!avatar.equals(currentInfo.getAvatar())) {
525 currentInfo = NewBubbleInfo.from(currentInfo).setAvatar(avatar).build();
yueg02dd4932018-01-25 14:17:06 -0800526 viewHolder.getPrimaryAvatar().setBackground(currentInfo.getAvatar());
yuega5a08d82017-10-31 14:11:53 -0700527 }
528 }
529
Eric Erfanian938468d2017-10-24 14:05:52 -0700530 /** Returns the currently displayed NewBubbleInfo */
531 public NewBubbleInfo getBubbleInfo() {
532 return currentInfo;
533 }
534
535 /**
yueg6518fdb2018-01-09 17:30:36 -0800536 * Display text. The bubble's drawer is not expandable while text is showing, and the drawer will
537 * be closed if already open.
Eric Erfanian938468d2017-10-24 14:05:52 -0700538 *
539 * @param text the text to display to the user
540 */
541 public void showText(@NonNull CharSequence text) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700542 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800543 startCollapse(CollapseEnd.NOTHING, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700544 }
yueg6518fdb2018-01-09 17:30:36 -0800545 Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
Eric Erfanian938468d2017-10-24 14:05:52 -0700546 }
547
Eric Erfanian938468d2017-10-24 14:05:52 -0700548 @Nullable
549 Integer getGravityOverride() {
550 return overrideGravity;
551 }
552
553 void onMoveStart() {
yueg81a77ff2017-12-05 10:29:03 -0800554 if (viewHolder.getExpandedView().getVisibility() == View.VISIBLE) {
555 viewHolder.setDrawerVisibility(View.INVISIBLE);
556 }
557 expanded = false;
558 savedYPosition = -1;
559
Eric Erfanian938468d2017-10-24 14:05:52 -0700560 viewHolder
yueg02dd4932018-01-25 14:17:06 -0800561 .getPrimaryAvatar()
Eric Erfanian938468d2017-10-24 14:05:52 -0700562 .animate()
563 .translationZ(
yuegc6deafc2017-11-06 16:42:13 -0800564 context
565 .getResources()
566 .getDimensionPixelOffset(R.dimen.bubble_dragging_elevation_change));
Eric Erfanian938468d2017-10-24 14:05:52 -0700567 }
568
569 void onMoveFinish() {
yueg02dd4932018-01-25 14:17:06 -0800570 viewHolder.getPrimaryAvatar().animate().translationZ(0);
Eric Erfanian938468d2017-10-24 14:05:52 -0700571 }
572
573 void primaryButtonClick() {
Eric Erfanian938468d2017-10-24 14:05:52 -0700574 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800575 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
576 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700577 } else {
yuegf539f782017-12-18 16:20:58 -0800578 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_EXPAND);
579 expand();
Eric Erfanian938468d2017-10-24 14:05:52 -0700580 }
581 }
582
yueg81a77ff2017-12-05 10:29:03 -0800583 void onLeftRightSwitch(boolean onRight) {
yueg87111362017-12-08 12:45:50 -0800584 // Move primary icon to the other side so it's not partially hiden
yueg81a77ff2017-12-05 10:29:03 -0800585 View primaryIcon = viewHolder.getPrimaryIcon();
yueg87111362017-12-08 12:45:50 -0800586 primaryIcon.animate().translationX(onRight ? -primaryIconMoveDistance : 0).start();
yueg81a77ff2017-12-05 10:29:03 -0800587 }
588
Eric Erfanian938468d2017-10-24 14:05:52 -0700589 LayoutParams getWindowParams() {
590 return windowParams;
591 }
592
593 View getRootView() {
594 return viewHolder.getRoot();
595 }
596
597 /**
598 * Hide the bubble if visible. Will run a short exit animation and before hiding, and {@code
599 * afterHiding} after hiding. If the bubble is currently showing text, will hide after the text is
600 * done displaying. If the bubble is not visible this method does nothing.
601 */
yueg87111362017-12-08 12:45:50 -0800602 @VisibleForTesting
603 void hideHelper(Runnable afterHiding) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700604 if (visibility == Visibility.HIDDEN || visibility == Visibility.EXITING) {
605 return;
606 }
607
608 // Make bubble non clickable to prevent further buggy actions
609 viewHolder.setChildClickable(false);
610
yueg349ac602018-01-17 09:02:45 -0800611 if (visibility == Visibility.ENTERING) {
612 enterAnimatorSet.removeAllListeners();
613 enterAnimatorSet.cancel();
614 enterAnimatorSet = null;
615 afterHiding.run();
616 return;
617 }
618
yueg6518fdb2018-01-09 17:30:36 -0800619 if (collapseAnimatorSet != null) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700620 collapseEndAction = CollapseEnd.HIDE;
621 return;
622 }
623
624 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800625 startCollapse(CollapseEnd.HIDE, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700626 return;
627 }
628
629 visibility = Visibility.EXITING;
yueg87111362017-12-08 12:45:50 -0800630
631 // Hide bubble animation: scale the whole bubble to 0, and change avatar+icon's alpha to 0
632 ObjectAnimator scaleXAnimator =
633 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 0);
634 ObjectAnimator scaleYAnimator =
635 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 0);
636 ObjectAnimator avatarAlphaAnimator =
637 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 0);
638 ObjectAnimator iconAlphaAnimator =
639 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 0);
640 exitAnimatorSet = new AnimatorSet();
641 exitAnimatorSet.playTogether(
642 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
643 exitAnimatorSet.setInterpolator(new AnticipateInterpolator());
yueg6518fdb2018-01-09 17:30:36 -0800644 exitAnimatorSet.setDuration(HIDE_BUBBLE_ANIMATION_DURATION);
yueg87111362017-12-08 12:45:50 -0800645 exitAnimatorSet.addListener(
646 new AnimatorListenerAdapter() {
647 @Override
yuega235e132017-12-13 14:13:57 -0800648 public void onAnimationStart(Animator animation) {
649 viewHolder.getPrimaryButton().setAccessibilityDelegate(null);
650 }
651
652 @Override
yueg87111362017-12-08 12:45:50 -0800653 public void onAnimationEnd(Animator animation) {
654 afterHiding.run();
655 }
656 });
657 exitAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700658 }
659
660 private void reset() {
661 viewHolder = new ViewHolder(viewHolder.getRoot().getContext());
662 update();
663 }
664
665 private void update() {
yuegbf7e8852018-01-23 10:00:15 -0800666 // The value may change on display size changed.
667 primaryIconMoveDistance =
668 context.getResources().getDimensionPixelSize(R.dimen.bubble_size)
669 - context.getResources().getDimensionPixelSize(R.dimen.bubble_small_icon_size);
yueg02dd4932018-01-25 14:17:06 -0800670
671 // Avatar
672 viewHolder.getPrimaryAvatar().setBackground(currentInfo.getAvatar());
yuegbf7e8852018-01-23 10:00:15 -0800673
yuega5a08d82017-10-31 14:11:53 -0700674 // Small icon
yueg84ac49b2017-11-01 16:22:28 -0700675 Drawable smallIconBackgroundCircle =
676 context
677 .getResources()
678 .getDrawable(R.drawable.bubble_shape_circle_small, context.getTheme());
679 smallIconBackgroundCircle.setTint(context.getColor(R.color.bubble_button_color_blue));
680 viewHolder.getPrimaryIcon().setBackground(smallIconBackgroundCircle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700681 viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon());
Eric Erfanian938468d2017-10-24 14:05:52 -0700682
yuega5a08d82017-10-31 14:11:53 -0700683 updatePrimaryIconAnimation();
Eric Erfanian938468d2017-10-24 14:05:52 -0700684 updateButtonStates();
685 }
686
687 private void updatePrimaryIconAnimation() {
688 Drawable drawable = viewHolder.getPrimaryIcon().getDrawable();
689 if (drawable instanceof Animatable) {
690 if (isVisible()) {
691 ((Animatable) drawable).start();
692 } else {
693 ((Animatable) drawable).stop();
694 }
695 }
696 }
697
698 private void updateButtonStates() {
yueg84ac49b2017-11-01 16:22:28 -0700699 configureButton(currentInfo.getActions().get(0), viewHolder.getFullScreenButton());
700 configureButton(currentInfo.getActions().get(1), viewHolder.getMuteButton());
701 configureButton(currentInfo.getActions().get(2), viewHolder.getAudioRouteButton());
702 configureButton(currentInfo.getActions().get(3), viewHolder.getEndCallButton());
Eric Erfanian938468d2017-10-24 14:05:52 -0700703 }
704
yueg84ac49b2017-11-01 16:22:28 -0700705 private void configureButton(Action action, NewCheckableButton button) {
yueg07e323c2017-12-19 16:05:47 -0800706 boolean isRtl =
707 TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
708 if (isRtl) {
709 button.setCompoundDrawablesWithIntrinsicBounds(null, null, action.getIconDrawable(), null);
710 } else {
711 button.setCompoundDrawablesWithIntrinsicBounds(action.getIconDrawable(), null, null, null);
712 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700713 button.setChecked(action.isChecked());
yueg2a422f72018-01-24 15:20:10 -0800714 button.setCheckable(action.isCheckable());
yueg84ac49b2017-11-01 16:22:28 -0700715 button.setText(action.getName());
yuega235e132017-12-13 14:13:57 -0800716 button.setContentDescription(action.getName());
Eric Erfanian938468d2017-10-24 14:05:52 -0700717 button.setOnClickListener(v -> doAction(action));
718 }
719
720 private void doAction(Action action) {
721 try {
722 action.getIntent().send();
723 } catch (CanceledException e) {
724 throw new RuntimeException(e);
725 }
726 }
727
yueg81a77ff2017-12-05 10:29:03 -0800728 /**
729 * Create a new ViewHolder object to replace the old one.It only happens when not moving and
730 * collapsed.
731 */
732 void replaceViewHolder() {
733 LogUtil.enterBlock("NewBubble.replaceViewHolder");
yuegf473e1d2018-01-02 16:23:14 -0800734 // Don't do it. If windowParams is null, either we haven't initialized it or we set it to null.
735 // There is no need to recreate bubble.
736 if (windowParams == null) {
737 return;
738 }
739
Eric Erfanian938468d2017-10-24 14:05:52 -0700740 ViewHolder oldViewHolder = viewHolder;
Eric Erfanian938468d2017-10-24 14:05:52 -0700741
yueg81a77ff2017-12-05 10:29:03 -0800742 // Create a new ViewHolder and copy needed info.
743 viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext());
yueg87111362017-12-08 12:45:50 -0800744 viewHolder.getPrimaryIcon().setX(isDrawingFromRight() ? 0 : primaryIconMoveDistance);
yueg81a77ff2017-12-05 10:29:03 -0800745 viewHolder
746 .getPrimaryIcon()
yueg87111362017-12-08 12:45:50 -0800747 .setTranslationX(isDrawingFromRight() ? -primaryIconMoveDistance : 0);
yuega235e132017-12-13 14:13:57 -0800748 setPrimaryButtonAccessibilityAction(
749 context.getString(R.string.a11y_bubble_primary_button_expand_action));
Eric Erfanian938468d2017-10-24 14:05:52 -0700750
yueg81a77ff2017-12-05 10:29:03 -0800751 update();
752
753 // Add new view at its horizontal boundary
Eric Erfanian938468d2017-10-24 14:05:52 -0700754 ViewGroup root = viewHolder.getRoot();
yueg81a77ff2017-12-05 10:29:03 -0800755 windowParams.x = leftBoundary;
756 windowParams.gravity = Gravity.TOP | (isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT);
Eric Erfanian938468d2017-10-24 14:05:52 -0700757 windowManager.addView(root, windowParams);
yueg81a77ff2017-12-05 10:29:03 -0800758
759 // Remove the old view after delay
Eric Erfanian938468d2017-10-24 14:05:52 -0700760 root.getViewTreeObserver()
761 .addOnPreDrawListener(
762 new OnPreDrawListener() {
763 @Override
764 public boolean onPreDraw() {
765 root.getViewTreeObserver().removeOnPreDrawListener(this);
766 // Wait a bit before removing the old view; make sure the new one has drawn over it.
767 handler.postDelayed(
768 () -> windowManager.removeView(oldViewHolder.getRoot()),
769 WINDOW_REDRAW_DELAY_MILLIS);
770 return true;
771 }
772 });
773 }
774
yueg81a77ff2017-12-05 10:29:03 -0800775 int getDrawerVisibility() {
776 return viewHolder.getExpandedView().getVisibility();
Eric Erfanian938468d2017-10-24 14:05:52 -0700777 }
778
yuegf473e1d2018-01-02 16:23:14 -0800779 void bottomActionDismiss() {
780 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_DISMISS);
781 // Create bubble at default location at next time
782 hideAndReset();
783 windowParams = null;
784 }
785
786 void bottomActionEndCall() {
787 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_END_CALL);
yueg6518fdb2018-01-09 17:30:36 -0800788 DialerCall call = getCall();
789 if (call != null) {
790 call.disconnect();
791 }
yuegf473e1d2018-01-02 16:23:14 -0800792 }
793
Eric Erfanian938468d2017-10-24 14:05:52 -0700794 private boolean isDrawingFromRight() {
795 return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
796 }
797
798 private void setFocused(boolean focused) {
799 if (focused) {
800 windowParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
801 } else {
802 windowParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
803 }
804 windowManager.updateViewLayout(getRootView(), windowParams);
805 }
806
807 private void defaultAfterHidingAnimation() {
yueg87111362017-12-08 12:45:50 -0800808 exitAnimatorSet = null;
809 viewHolder.getPrimaryButton().setVisibility(View.INVISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700810 windowManager.removeView(viewHolder.getRoot());
811 visibility = Visibility.HIDDEN;
812
813 updatePrimaryIconAnimation();
814 }
815
yueg81a77ff2017-12-05 10:29:03 -0800816 private void logBasicOrCallImpression(DialerImpression.Type impressionType) {
yuegf473e1d2018-01-02 16:23:14 -0800817 DialerCall call = getCall();
yueg81a77ff2017-12-05 10:29:03 -0800818 if (call != null) {
819 Logger.get(context)
820 .logCallImpression(impressionType, call.getUniqueCallId(), call.getTimeAddedMs());
821 } else {
822 Logger.get(context).logImpression(impressionType);
823 }
824 }
825
yuegf473e1d2018-01-02 16:23:14 -0800826 private DialerCall getCall() {
827 // Bubble is shown for outgoing, active or background call
828 DialerCall call = CallList.getInstance().getOutgoingCall();
829 if (call == null) {
830 call = CallList.getInstance().getActiveOrBackgroundCall();
831 }
832 return call;
833 }
834
yuega235e132017-12-13 14:13:57 -0800835 private void setPrimaryButtonAccessibilityAction(String description) {
836 viewHolder
837 .getPrimaryButton()
838 .setAccessibilityDelegate(
839 new AccessibilityDelegate() {
840 @Override
841 public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) {
842 super.onInitializeAccessibilityNodeInfo(v, info);
843
844 AccessibilityAction clickAction =
845 new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, description);
846 info.addAction(clickAction);
847 }
848 });
849 }
850
yueg0b4755c2017-12-18 10:01:03 -0800851 private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider(View view) {
852 int startRectX = isDrawingFromRight() ? view.getMeasuredWidth() : 0;
853 Rect startRect = new Rect(startRectX, 0, startRectX, 0);
854 Rect endRect = new Rect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
855
856 float bubbleRadius = context.getResources().getDimension(R.dimen.bubble_radius);
857 return new RoundedRectRevealOutlineProvider(bubbleRadius, bubbleRadius, startRect, endRect);
858 }
859
860 private ValueAnimator createBubbleMoveAnimator(int startX, int endX, int startY, float k) {
861 ValueAnimator xValueAnimator = ValueAnimator.ofFloat(startX, endX);
862 xValueAnimator.setInterpolator(new LinearOutSlowInInterpolator());
863 xValueAnimator.addUpdateListener(
864 (valueAnimator) -> {
yueg02dd4932018-01-25 14:17:06 -0800865 if (windowParams == null) {
866 return;
867 }
yueg0b4755c2017-12-18 10:01:03 -0800868 // Update windowParams and the root layout.
869 // We can't do ViewPropertyAnimation since it clips children.
870 float newX = (float) valueAnimator.getAnimatedValue();
871 if (k != 0) {
872 windowParams.y = startY + (int) (Math.abs(newX - (float) startX) * k);
873 }
874 windowParams.x = (int) newX;
875 windowManager.updateViewLayout(viewHolder.getRoot(), windowParams);
876 });
877 return xValueAnimator;
878 }
879
Eric Erfanian938468d2017-10-24 14:05:52 -0700880 @VisibleForTesting
881 class ViewHolder {
882
yueg0b4755c2017-12-18 10:01:03 -0800883 private NewMoveHandler moveHandler;
Eric Erfanian938468d2017-10-24 14:05:52 -0700884 private final NewWindowRoot root;
yueg7e421822018-01-19 11:39:03 -0800885 private final View primaryButton;
Eric Erfanian938468d2017-10-24 14:05:52 -0700886 private final ImageView primaryIcon;
yuega5a08d82017-10-31 14:11:53 -0700887 private final ImageView primaryAvatar;
yueg0b4755c2017-12-18 10:01:03 -0800888 private final View arrow;
Eric Erfanian938468d2017-10-24 14:05:52 -0700889
890 private final NewCheckableButton fullScreenButton;
891 private final NewCheckableButton muteButton;
892 private final NewCheckableButton audioRouteButton;
893 private final NewCheckableButton endCallButton;
894 private final View expandedView;
895
896 public ViewHolder(Context context) {
897 // Window root is not in the layout file so that the inflater has a view to inflate into
898 this.root = new NewWindowRoot(context);
899 LayoutInflater inflater = LayoutInflater.from(root.getContext());
900 View contentView = inflater.inflate(R.layout.new_bubble_base, root, true);
901 expandedView = contentView.findViewById(R.id.bubble_expanded_layout);
902 primaryButton = contentView.findViewById(R.id.bubble_button_primary);
yuega5a08d82017-10-31 14:11:53 -0700903 primaryAvatar = contentView.findViewById(R.id.bubble_icon_avatar);
Eric Erfanian938468d2017-10-24 14:05:52 -0700904 primaryIcon = contentView.findViewById(R.id.bubble_icon_primary);
yueg0b4755c2017-12-18 10:01:03 -0800905 arrow = contentView.findViewById(R.id.bubble_triangle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700906
907 fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen);
908 muteButton = contentView.findViewById(R.id.bubble_button_mute);
909 audioRouteButton = contentView.findViewById(R.id.bubble_button_audio_route);
910 endCallButton = contentView.findViewById(R.id.bubble_button_end_call);
911
912 root.setOnBackPressedListener(
913 () -> {
914 if (visibility == Visibility.SHOWING && expanded) {
yuegf539f782017-12-18 16:20:58 -0800915 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
916 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700917 return true;
918 }
919 return false;
920 });
921 root.setOnConfigurationChangedListener(
922 (configuration) -> {
yueg6518fdb2018-01-09 17:30:36 -0800923 if (expanded) {
yueg10f6e822018-01-17 15:32:18 -0800924 startCollapse(CollapseEnd.NOTHING, false /* shouldRecoverYPosition */);
yueg6518fdb2018-01-09 17:30:36 -0800925 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700926 // The values in the current MoveHandler may be stale, so replace it. Then ensure the
yuegbf7e8852018-01-23 10:00:15 -0800927 // Window is in bounds, and redraw the changes
Eric Erfanian938468d2017-10-24 14:05:52 -0700928 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
929 moveHandler.snapToBounds();
yuegbf7e8852018-01-23 10:00:15 -0800930 replaceViewHolder();
Eric Erfanian938468d2017-10-24 14:05:52 -0700931 });
932 root.setOnTouchListener(
933 (v, event) -> {
934 if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
yuegf539f782017-12-18 16:20:58 -0800935 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
936 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700937 return true;
938 }
939 return false;
940 });
941 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
942 }
943
944 private void setChildClickable(boolean clickable) {
945 fullScreenButton.setClickable(clickable);
946 muteButton.setClickable(clickable);
947 audioRouteButton.setClickable(clickable);
948 endCallButton.setClickable(clickable);
yueg0b4755c2017-12-18 10:01:03 -0800949 setPrimaryButtonClickable(clickable);
950 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700951
yueg0b4755c2017-12-18 10:01:03 -0800952 private void setPrimaryButtonClickable(boolean clickable) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700953 moveHandler.setClickable(clickable);
954 }
955
yueg81a77ff2017-12-05 10:29:03 -0800956 public int getMoveUpDistance() {
957 int deltaAllowed =
958 expandedView.getHeight()
959 - context
960 .getResources()
961 .getDimensionPixelOffset(R.dimen.bubble_button_padding_vertical)
962 * 2;
963 return moveHandler.getMoveUpDistance(deltaAllowed);
964 }
965
Eric Erfanian938468d2017-10-24 14:05:52 -0700966 public ViewGroup getRoot() {
967 return root;
968 }
969
yueg7e421822018-01-19 11:39:03 -0800970 public View getPrimaryButton() {
Eric Erfanian938468d2017-10-24 14:05:52 -0700971 return primaryButton;
972 }
973
974 public ImageView getPrimaryIcon() {
975 return primaryIcon;
976 }
977
yuega5a08d82017-10-31 14:11:53 -0700978 public ImageView getPrimaryAvatar() {
979 return primaryAvatar;
980 }
981
Eric Erfanian938468d2017-10-24 14:05:52 -0700982 public View getExpandedView() {
983 return expandedView;
984 }
985
yueg0b4755c2017-12-18 10:01:03 -0800986 public View getArrow() {
987 return arrow;
988 }
989
Eric Erfanian938468d2017-10-24 14:05:52 -0700990 public NewCheckableButton getFullScreenButton() {
991 return fullScreenButton;
992 }
993
994 public NewCheckableButton getMuteButton() {
995 return muteButton;
996 }
997
998 public NewCheckableButton getAudioRouteButton() {
999 return audioRouteButton;
1000 }
1001
1002 public NewCheckableButton getEndCallButton() {
1003 return endCallButton;
1004 }
1005
1006 public void setDrawerVisibility(int visibility) {
1007 expandedView.setVisibility(visibility);
1008 }
1009
1010 public boolean isMoving() {
1011 return moveHandler.isMoving();
1012 }
1013
1014 public void undoGravityOverride() {
1015 moveHandler.undoGravityOverride();
1016 }
1017 }
Eric Erfanian938468d2017-10-24 14:05:52 -07001018}