blob: 8b188ba335b3387e53e99f6990f85c779400c795 [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;
yuegbf7e8852018-01-23 10:00:15 -080028import android.graphics.Outline;
Eric Erfanian938468d2017-10-24 14:05:52 -070029import android.graphics.PixelFormat;
yueg0b4755c2017-12-18 10:01:03 -080030import android.graphics.Rect;
Eric Erfanian938468d2017-10-24 14:05:52 -070031import android.graphics.drawable.Animatable;
32import android.graphics.drawable.Drawable;
Eric Erfanian938468d2017-10-24 14:05:52 -070033import android.net.Uri;
34import android.os.Handler;
35import android.provider.Settings;
Eric Erfanian938468d2017-10-24 14:05:52 -070036import android.support.annotation.IntDef;
37import android.support.annotation.NonNull;
38import android.support.annotation.Nullable;
39import android.support.annotation.VisibleForTesting;
Eric Erfanian938468d2017-10-24 14:05:52 -070040import android.support.v4.os.BuildCompat;
Eric Erfanian938468d2017-10-24 14:05:52 -070041import android.support.v4.view.animation.LinearOutSlowInInterpolator;
yueg07e323c2017-12-19 16:05:47 -080042import android.text.TextUtils;
Eric Erfanian938468d2017-10-24 14:05:52 -070043import android.view.ContextThemeWrapper;
44import android.view.Gravity;
45import android.view.LayoutInflater;
46import android.view.MotionEvent;
47import android.view.View;
yuega235e132017-12-13 14:13:57 -080048import android.view.View.AccessibilityDelegate;
Eric Erfanian938468d2017-10-24 14:05:52 -070049import android.view.ViewGroup;
yuegbf7e8852018-01-23 10:00:15 -080050import android.view.ViewOutlineProvider;
Eric Erfanian938468d2017-10-24 14:05:52 -070051import android.view.ViewTreeObserver.OnPreDrawListener;
52import android.view.WindowManager;
53import android.view.WindowManager.LayoutParams;
yuega235e132017-12-13 14:13:57 -080054import android.view.accessibility.AccessibilityNodeInfo;
55import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
yueg0b4755c2017-12-18 10:01:03 -080056import android.view.animation.AccelerateDecelerateInterpolator;
Eric Erfanian938468d2017-10-24 14:05:52 -070057import android.view.animation.AnticipateInterpolator;
58import android.view.animation.OvershootInterpolator;
59import android.widget.ImageView;
yuegf473e1d2018-01-02 16:23:14 -080060import android.widget.Toast;
yueg81a77ff2017-12-05 10:29:03 -080061import com.android.dialer.common.LogUtil;
62import com.android.dialer.logging.DialerImpression;
63import com.android.dialer.logging.Logger;
yuega5a08d82017-10-31 14:11:53 -070064import com.android.dialer.util.DrawableConverter;
yueg81a77ff2017-12-05 10:29:03 -080065import com.android.incallui.call.CallList;
66import com.android.incallui.call.DialerCall;
Eric Erfanian938468d2017-10-24 14:05:52 -070067import com.android.newbubble.NewBubbleInfo.Action;
68import java.lang.annotation.Retention;
69import java.lang.annotation.RetentionPolicy;
70import java.util.List;
yueg07e323c2017-12-19 16:05:47 -080071import java.util.Locale;
Eric Erfanian938468d2017-10-24 14:05:52 -070072
73/**
74 * Creates and manages a bubble window from information in a {@link NewBubbleInfo}. Before creating,
75 * be sure to check whether bubbles may be shown using {@link #canShowBubbles(Context)} and request
76 * permission if necessary ({@link #getRequestPermissionIntent(Context)} is provided for
77 * convenience)
78 */
79public class NewBubble {
80 // This class has some odd behavior that is not immediately obvious in order to avoid jank when
81 // resizing. See http://go/bubble-resize for details.
82
Eric Erfanian938468d2017-10-24 14:05:52 -070083 // How long the new window should show before destroying the old one during resize operations.
84 // This ensures the new window has had time to draw first.
85 private static final int WINDOW_REDRAW_DELAY_MILLIS = 50;
86
yueg0b4755c2017-12-18 10:01:03 -080087 private static final int EXPAND_AND_COLLAPSE_ANIMATION_DURATION = 200;
yueg6518fdb2018-01-09 17:30:36 -080088 private static final int HIDE_BUBBLE_ANIMATION_DURATION = 250;
yueg0b4755c2017-12-18 10:01:03 -080089
Eric Erfanian938468d2017-10-24 14:05:52 -070090 private static Boolean canShowBubblesForTesting = null;
91
yueg0b4755c2017-12-18 10:01:03 -080092 private final AccelerateDecelerateInterpolator accelerateDecelerateInterpolator =
93 new AccelerateDecelerateInterpolator();
94
Eric Erfanian938468d2017-10-24 14:05:52 -070095 private final Context context;
96 private final WindowManager windowManager;
97
98 private final Handler handler;
99 private LayoutParams windowParams;
100
101 // Initialized in factory method
102 @SuppressWarnings("NullableProblems")
103 @NonNull
104 private NewBubbleInfo currentInfo;
105
yueg349ac602018-01-17 09:02:45 -0800106 @VisibleForTesting @Visibility int visibility;
Eric Erfanian938468d2017-10-24 14:05:52 -0700107 private boolean expanded;
Eric Erfanian938468d2017-10-24 14:05:52 -0700108 private CharSequence textAfterShow;
109 private int collapseEndAction;
110
yueg81a77ff2017-12-05 10:29:03 -0800111 ViewHolder viewHolder;
yueg6518fdb2018-01-09 17:30:36 -0800112 private AnimatorSet collapseAnimatorSet;
Eric Erfanian938468d2017-10-24 14:05:52 -0700113 private Integer overrideGravity;
yueg87111362017-12-08 12:45:50 -0800114 @VisibleForTesting AnimatorSet exitAnimatorSet;
yueg349ac602018-01-17 09:02:45 -0800115 @VisibleForTesting AnimatorSet enterAnimatorSet;
Eric Erfanian938468d2017-10-24 14:05:52 -0700116
yuegbf7e8852018-01-23 10:00:15 -0800117 private int primaryIconMoveDistance;
yueg87111362017-12-08 12:45:50 -0800118 private final int leftBoundary;
yueg81a77ff2017-12-05 10:29:03 -0800119 private int savedYPosition = -1;
120
Eric Erfanian938468d2017-10-24 14:05:52 -0700121 /** Type of action after bubble collapse */
122 @Retention(RetentionPolicy.SOURCE)
123 @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE})
yueg349ac602018-01-17 09:02:45 -0800124 private @interface CollapseEnd {
Eric Erfanian938468d2017-10-24 14:05:52 -0700125 int NOTHING = 0;
126 int HIDE = 1;
127 }
128
129 @Retention(RetentionPolicy.SOURCE)
yueg349ac602018-01-17 09:02:45 -0800130 @VisibleForTesting
Eric Erfanian938468d2017-10-24 14:05:52 -0700131 @IntDef({Visibility.ENTERING, Visibility.SHOWING, Visibility.EXITING, Visibility.HIDDEN})
yueg349ac602018-01-17 09:02:45 -0800132 @interface Visibility {
Eric Erfanian938468d2017-10-24 14:05:52 -0700133 int HIDDEN = 0;
134 int ENTERING = 1;
135 int SHOWING = 2;
136 int EXITING = 3;
137 }
138
139 /** Indicate bubble expansion state. */
140 @Retention(RetentionPolicy.SOURCE)
141 @IntDef({ExpansionState.START_EXPANDING, ExpansionState.START_COLLAPSING})
142 public @interface ExpansionState {
143 // TODO(yueg): add more states when needed
144 int START_EXPANDING = 0;
145 int START_COLLAPSING = 1;
146 }
147
148 /**
149 * Determines whether bubbles can be shown based on permissions obtained. This should be checked
150 * before attempting to create a Bubble.
151 *
152 * @return true iff bubbles are able to be shown.
153 * @see Settings#canDrawOverlays(Context)
154 */
155 public static boolean canShowBubbles(@NonNull Context context) {
156 return canShowBubblesForTesting != null
157 ? canShowBubblesForTesting
158 : Settings.canDrawOverlays(context);
159 }
160
161 @VisibleForTesting(otherwise = VisibleForTesting.NONE)
162 public static void setCanShowBubblesForTesting(boolean canShowBubbles) {
163 canShowBubblesForTesting = canShowBubbles;
164 }
165
166 /** Returns an Intent to request permission to show overlays */
167 @NonNull
168 public static Intent getRequestPermissionIntent(@NonNull Context context) {
169 return new Intent(
170 Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
171 Uri.fromParts("package", context.getPackageName(), null));
172 }
173
174 /** Creates instances of Bubble. The default implementation just calls the constructor. */
175 @VisibleForTesting
176 public interface BubbleFactory {
177 NewBubble createBubble(@NonNull Context context, @NonNull Handler handler);
178 }
179
180 private static BubbleFactory bubbleFactory = NewBubble::new;
181
182 public static NewBubble createBubble(@NonNull Context context, @NonNull NewBubbleInfo info) {
183 NewBubble bubble = bubbleFactory.createBubble(context, new Handler());
184 bubble.setBubbleInfo(info);
185 return bubble;
186 }
187
188 @VisibleForTesting
189 public static void setBubbleFactory(@NonNull BubbleFactory bubbleFactory) {
190 NewBubble.bubbleFactory = bubbleFactory;
191 }
192
193 @VisibleForTesting
194 public static void resetBubbleFactory() {
195 NewBubble.bubbleFactory = NewBubble::new;
196 }
197
198 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
199 NewBubble(@NonNull Context context, @NonNull Handler handler) {
200 context = new ContextThemeWrapper(context, R.style.Theme_AppCompat);
201 this.context = context;
202 this.handler = handler;
203 windowManager = context.getSystemService(WindowManager.class);
204
205 viewHolder = new ViewHolder(context);
yueg81a77ff2017-12-05 10:29:03 -0800206
207 leftBoundary =
208 context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal)
209 - context
210 .getResources()
211 .getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal);
yueg87111362017-12-08 12:45:50 -0800212 primaryIconMoveDistance =
213 context.getResources().getDimensionPixelSize(R.dimen.bubble_size)
214 - context.getResources().getDimensionPixelSize(R.dimen.bubble_small_icon_size);
Eric Erfanian938468d2017-10-24 14:05:52 -0700215 }
216
217 /** Expands the main bubble menu. */
yuegf539f782017-12-18 16:20:58 -0800218 public void expand() {
yuega235e132017-12-13 14:13:57 -0800219 setPrimaryButtonAccessibilityAction(
220 context.getString(R.string.a11y_bubble_primary_button_collapse_action));
yueg0b4755c2017-12-18 10:01:03 -0800221
yueg81a77ff2017-12-05 10:29:03 -0800222 viewHolder.setDrawerVisibility(View.INVISIBLE);
yueg0b4755c2017-12-18 10:01:03 -0800223 viewHolder.getArrow().setVisibility(View.INVISIBLE);
224 // No click during animation to avoid jank.
225 viewHolder.setPrimaryButtonClickable(false);
226
Eric Erfanian938468d2017-10-24 14:05:52 -0700227 View expandedView = viewHolder.getExpandedView();
228 expandedView
229 .getViewTreeObserver()
230 .addOnPreDrawListener(
231 new OnPreDrawListener() {
232 @Override
233 public boolean onPreDraw() {
yueg81a77ff2017-12-05 10:29:03 -0800234 // Move the whole bubble up so that expanded view is still in screen
235 int moveUpDistance = viewHolder.getMoveUpDistance();
236 if (moveUpDistance != 0) {
237 savedYPosition = windowParams.y;
238 }
239
yueg0b4755c2017-12-18 10:01:03 -0800240 // Animation 1: animate x-move and y-move (if needed) together
yueg81a77ff2017-12-05 10:29:03 -0800241 int deltaX =
242 (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
yueg0b4755c2017-12-18 10:01:03 -0800243 float k = -(float) moveUpDistance / deltaX;
yueg81a77ff2017-12-05 10:29:03 -0800244 if (isDrawingFromRight()) {
245 deltaX = -deltaX;
246 }
yueg0b4755c2017-12-18 10:01:03 -0800247 ValueAnimator xValueAnimator =
248 createBubbleMoveAnimator(
249 windowParams.x - deltaX, windowParams.x, windowParams.y, k);
yueg81a77ff2017-12-05 10:29:03 -0800250
yueg0b4755c2017-12-18 10:01:03 -0800251 // Show expanded view
252 expandedView.setVisibility(View.VISIBLE);
yueg81a77ff2017-12-05 10:29:03 -0800253
yueg0b4755c2017-12-18 10:01:03 -0800254 // Animator 2: reveal expanded view from top left or top right
255 View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
256 ValueAnimator revealAnim =
257 createOpenCloseOutlineProvider(expandedMenu)
258 .createRevealAnimator(expandedMenu, false);
259 revealAnim.setInterpolator(accelerateDecelerateInterpolator);
260
261 // Animator 3: expanded view fade in
262 Animator fadeIn = ObjectAnimator.ofFloat(expandedView, "alpha", 0, 1);
263 fadeIn.setInterpolator(accelerateDecelerateInterpolator);
264
265 // Play all animation together
266 AnimatorSet expandAnimatorSet = new AnimatorSet();
267 expandAnimatorSet.playTogether(revealAnim, fadeIn, xValueAnimator);
268 expandAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
269 expandAnimatorSet.addListener(
yueg87111362017-12-08 12:45:50 -0800270 new AnimatorListenerAdapter() {
yueg81a77ff2017-12-05 10:29:03 -0800271 @Override
272 public void onAnimationEnd(Animator animation) {
yueg0b4755c2017-12-18 10:01:03 -0800273 // Show arrow after animation
274 viewHolder.getArrow().setVisibility(View.VISIBLE);
275 // Safe to click primary button now
276 viewHolder.setPrimaryButtonClickable(true);
yueg81a77ff2017-12-05 10:29:03 -0800277 }
yueg81a77ff2017-12-05 10:29:03 -0800278 });
yueg0b4755c2017-12-18 10:01:03 -0800279 expandAnimatorSet.start();
yueg81a77ff2017-12-05 10:29:03 -0800280
Eric Erfanian938468d2017-10-24 14:05:52 -0700281 expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
Eric Erfanian938468d2017-10-24 14:05:52 -0700282 return false;
283 }
284 });
285 setFocused(true);
286 expanded = true;
287 }
288
yueg81a77ff2017-12-05 10:29:03 -0800289 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
yuegf539f782017-12-18 16:20:58 -0800290 public void startCollapse(@CollapseEnd int endAction, boolean shouldRecoverYPosition) {
yueg81a77ff2017-12-05 10:29:03 -0800291 View expandedView = viewHolder.getExpandedView();
yueg6518fdb2018-01-09 17:30:36 -0800292 if (expandedView.getVisibility() != View.VISIBLE || collapseAnimatorSet != null) {
yueg81a77ff2017-12-05 10:29:03 -0800293 // Drawer is already collapsed or animation is running.
294 return;
295 }
296
297 overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT;
298 setFocused(false);
299
300 if (collapseEndAction == CollapseEnd.NOTHING) {
301 collapseEndAction = endAction;
302 }
yuega235e132017-12-13 14:13:57 -0800303 setPrimaryButtonAccessibilityAction(
304 context.getString(R.string.a11y_bubble_primary_button_expand_action));
yueg0b4755c2017-12-18 10:01:03 -0800305
306 // Hide arrow before animation
307 viewHolder.getArrow().setVisibility(View.INVISIBLE);
308
309 // No click during animation to avoid jank.
310 viewHolder.setPrimaryButtonClickable(false);
311
312 // Calculate animation values
313 int deltaX = (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
314 float k =
315 (savedYPosition != -1 && shouldRecoverYPosition)
316 ? (savedYPosition - windowParams.y) / (float) deltaX
317 : 0;
318 // The position is not useful after collapse
319 savedYPosition = -1;
320
321 // Animation 1: animate x-move and y-move (if needed) together
322 ValueAnimator xValueAnimator =
323 createBubbleMoveAnimator(windowParams.x, windowParams.x - deltaX, windowParams.y, k);
324
325 // Animator 2: hide expanded view to top left or top right
326 View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
327 ValueAnimator revealAnim =
328 createOpenCloseOutlineProvider(expandedMenu).createRevealAnimator(expandedMenu, true);
329 revealAnim.setInterpolator(accelerateDecelerateInterpolator);
330
331 // Animator 3: expanded view fade out
332 Animator fadeOut = ObjectAnimator.ofFloat(expandedView, "alpha", 1, 0);
333 fadeOut.setInterpolator(accelerateDecelerateInterpolator);
334
335 // Play all animation together
yueg6518fdb2018-01-09 17:30:36 -0800336 collapseAnimatorSet = new AnimatorSet();
yueg0b4755c2017-12-18 10:01:03 -0800337 collapseAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
338 collapseAnimatorSet.playTogether(revealAnim, fadeOut, xValueAnimator);
339 collapseAnimatorSet.addListener(
340 new AnimatorListenerAdapter() {
341 @Override
342 public void onAnimationEnd(Animator animation) {
yueg6518fdb2018-01-09 17:30:36 -0800343 collapseAnimatorSet = null;
yueg0b4755c2017-12-18 10:01:03 -0800344 expanded = false;
345
yueg0b4755c2017-12-18 10:01:03 -0800346 // If collapse on the right side, the primary button move left a bit after drawer
347 // visibility becoming GONE. To avoid it, we create a new ViewHolder.
348 // It also set primary button clickable back to true, so no need to reset manually.
349 replaceViewHolder();
350
yueg6518fdb2018-01-09 17:30:36 -0800351 // If this collapse was to come before a hide, do it now.
352 if (collapseEndAction == CollapseEnd.HIDE) {
353 hide();
354 collapseEndAction = CollapseEnd.NOTHING;
355 }
356
yueg0b4755c2017-12-18 10:01:03 -0800357 // Resume normal gravity after any resizing is done.
358 handler.postDelayed(
yueg81a77ff2017-12-05 10:29:03 -0800359 () -> {
yueg0b4755c2017-12-18 10:01:03 -0800360 overrideGravity = null;
361 if (!viewHolder.isMoving()) {
362 viewHolder.undoGravityOverride();
yueg81a77ff2017-12-05 10:29:03 -0800363 }
yueg0b4755c2017-12-18 10:01:03 -0800364 },
365 // Need to wait twice as long for resize and layout
366 WINDOW_REDRAW_DELAY_MILLIS * 2);
367 }
368 });
369 collapseAnimatorSet.start();
yueg81a77ff2017-12-05 10:29:03 -0800370 }
371
Eric Erfanian938468d2017-10-24 14:05:52 -0700372 /**
373 * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is
374 * already showing this method does nothing.
375 */
376 public void show() {
377 if (collapseEndAction == CollapseEnd.HIDE) {
378 // If show() was called while collapsing, make sure we don't hide after.
379 collapseEndAction = CollapseEnd.NOTHING;
380 }
381 if (visibility == Visibility.SHOWING || visibility == Visibility.ENTERING) {
382 return;
383 }
384
yueg2f664032018-01-10 11:30:30 -0800385 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_SHOW);
386
yueg07e323c2017-12-19 16:05:47 -0800387 boolean isRtl =
388 TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
Eric Erfanian938468d2017-10-24 14:05:52 -0700389 if (windowParams == null) {
390 // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O.
391 @SuppressWarnings("deprecation")
392 @SuppressLint("InlinedApi")
393 int type =
394 BuildCompat.isAtLeastO()
395 ? LayoutParams.TYPE_APPLICATION_OVERLAY
396 : LayoutParams.TYPE_PHONE;
397
398 windowParams =
399 new LayoutParams(
400 type,
401 LayoutParams.FLAG_NOT_TOUCH_MODAL
402 | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
403 | LayoutParams.FLAG_NOT_FOCUSABLE
404 | LayoutParams.FLAG_LAYOUT_NO_LIMITS,
405 PixelFormat.TRANSLUCENT);
yueg07e323c2017-12-19 16:05:47 -0800406 windowParams.gravity = Gravity.TOP | (isRtl ? Gravity.RIGHT : Gravity.LEFT);
yueg81a77ff2017-12-05 10:29:03 -0800407 windowParams.x = leftBoundary;
Eric Erfanian938468d2017-10-24 14:05:52 -0700408 windowParams.y = currentInfo.getStartingYPosition();
409 windowParams.height = LayoutParams.WRAP_CONTENT;
410 windowParams.width = LayoutParams.WRAP_CONTENT;
411 }
412
yueg87111362017-12-08 12:45:50 -0800413 if (exitAnimatorSet != null) {
414 exitAnimatorSet.removeAllListeners();
415 exitAnimatorSet.cancel();
416 exitAnimatorSet = null;
Eric Erfanian938468d2017-10-24 14:05:52 -0700417 } else {
418 windowManager.addView(viewHolder.getRoot(), windowParams);
yueg87111362017-12-08 12:45:50 -0800419 viewHolder.getPrimaryButton().setVisibility(View.VISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700420 viewHolder.getPrimaryButton().setScaleX(0);
421 viewHolder.getPrimaryButton().setScaleY(0);
yueg87111362017-12-08 12:45:50 -0800422 viewHolder.getPrimaryAvatar().setAlpha(0f);
423 viewHolder.getPrimaryIcon().setAlpha(0f);
yueg07e323c2017-12-19 16:05:47 -0800424 if (isRtl) {
425 onLeftRightSwitch(true);
426 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700427 }
428
429 viewHolder.setChildClickable(true);
430 visibility = Visibility.ENTERING;
yueg87111362017-12-08 12:45:50 -0800431
yuega235e132017-12-13 14:13:57 -0800432 setPrimaryButtonAccessibilityAction(
433 context.getString(R.string.a11y_bubble_primary_button_expand_action));
434
yueg87111362017-12-08 12:45:50 -0800435 // Show bubble animation: scale the whole bubble to 1, and change avatar+icon's alpha to 1
436 ObjectAnimator scaleXAnimator =
437 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 1);
438 ObjectAnimator scaleYAnimator =
439 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 1);
440 ObjectAnimator avatarAlphaAnimator =
441 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 1);
442 ObjectAnimator iconAlphaAnimator =
443 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 1);
yueg349ac602018-01-17 09:02:45 -0800444 enterAnimatorSet = new AnimatorSet();
yueg87111362017-12-08 12:45:50 -0800445 enterAnimatorSet.playTogether(
446 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
447 enterAnimatorSet.setInterpolator(new OvershootInterpolator());
448 enterAnimatorSet.addListener(
449 new AnimatorListenerAdapter() {
450 @Override
451 public void onAnimationEnd(Animator animation) {
452 visibility = Visibility.SHOWING;
453 // Show the queued up text, if available.
454 if (textAfterShow != null) {
455 showText(textAfterShow);
456 textAfterShow = null;
457 }
458 }
459 });
460 enterAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700461
462 updatePrimaryIconAnimation();
463 }
464
465 /** Hide the bubble. */
466 public void hide() {
Eric Erfanian938468d2017-10-24 14:05:52 -0700467 hideHelper(this::defaultAfterHidingAnimation);
468 }
469
470 /** Hide the bubble and reset {@viewHolder} to initial state */
471 public void hideAndReset() {
472 hideHelper(
473 () -> {
474 defaultAfterHidingAnimation();
475 reset();
476 });
477 }
478
479 /** Returns whether the bubble is currently visible */
480 public boolean isVisible() {
481 return visibility == Visibility.SHOWING
482 || visibility == Visibility.ENTERING
483 || visibility == Visibility.EXITING;
484 }
485
486 /**
487 * Set the info for this Bubble to display
488 *
489 * @param bubbleInfo the BubbleInfo to display in this Bubble.
490 */
491 public void setBubbleInfo(@NonNull NewBubbleInfo bubbleInfo) {
492 currentInfo = bubbleInfo;
493 update();
494 }
495
496 /**
497 * Update the state and behavior of actions.
498 *
499 * @param actions the new state of the bubble's actions
500 */
501 public void updateActions(@NonNull List<Action> actions) {
502 currentInfo = NewBubbleInfo.from(currentInfo).setActions(actions).build();
503 updateButtonStates();
504 }
505
yuega5a08d82017-10-31 14:11:53 -0700506 /**
507 * Update the avatar from photo.
508 *
509 * @param avatar the new photo avatar in the bubble's primary button
510 */
511 public void updatePhotoAvatar(@NonNull Drawable avatar) {
512 // Make it round
513 int bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
514 Drawable roundAvatar =
515 DrawableConverter.getRoundedDrawable(context, avatar, bubbleSize, bubbleSize);
516
517 updateAvatar(roundAvatar);
518 }
519
520 /**
521 * Update the avatar.
522 *
523 * @param avatar the new avatar in the bubble's primary button
524 */
525 public void updateAvatar(@NonNull Drawable avatar) {
526 if (!avatar.equals(currentInfo.getAvatar())) {
527 currentInfo = NewBubbleInfo.from(currentInfo).setAvatar(avatar).build();
528 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
529 }
530 }
531
Eric Erfanian938468d2017-10-24 14:05:52 -0700532 /** Returns the currently displayed NewBubbleInfo */
533 public NewBubbleInfo getBubbleInfo() {
534 return currentInfo;
535 }
536
537 /**
yueg6518fdb2018-01-09 17:30:36 -0800538 * Display text. The bubble's drawer is not expandable while text is showing, and the drawer will
539 * be closed if already open.
Eric Erfanian938468d2017-10-24 14:05:52 -0700540 *
541 * @param text the text to display to the user
542 */
543 public void showText(@NonNull CharSequence text) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700544 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800545 startCollapse(CollapseEnd.NOTHING, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700546 }
yueg6518fdb2018-01-09 17:30:36 -0800547 Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
Eric Erfanian938468d2017-10-24 14:05:52 -0700548 }
549
Eric Erfanian938468d2017-10-24 14:05:52 -0700550 @Nullable
551 Integer getGravityOverride() {
552 return overrideGravity;
553 }
554
555 void onMoveStart() {
yueg81a77ff2017-12-05 10:29:03 -0800556 if (viewHolder.getExpandedView().getVisibility() == View.VISIBLE) {
557 viewHolder.setDrawerVisibility(View.INVISIBLE);
558 }
559 expanded = false;
560 savedYPosition = -1;
561
Eric Erfanian938468d2017-10-24 14:05:52 -0700562 viewHolder
563 .getPrimaryButton()
564 .animate()
565 .translationZ(
yuegc6deafc2017-11-06 16:42:13 -0800566 context
567 .getResources()
568 .getDimensionPixelOffset(R.dimen.bubble_dragging_elevation_change));
Eric Erfanian938468d2017-10-24 14:05:52 -0700569 }
570
571 void onMoveFinish() {
572 viewHolder.getPrimaryButton().animate().translationZ(0);
Eric Erfanian938468d2017-10-24 14:05:52 -0700573 }
574
575 void primaryButtonClick() {
Eric Erfanian938468d2017-10-24 14:05:52 -0700576 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800577 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
578 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700579 } else {
yuegf539f782017-12-18 16:20:58 -0800580 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_EXPAND);
581 expand();
Eric Erfanian938468d2017-10-24 14:05:52 -0700582 }
583 }
584
yueg81a77ff2017-12-05 10:29:03 -0800585 void onLeftRightSwitch(boolean onRight) {
yueg87111362017-12-08 12:45:50 -0800586 // Move primary icon to the other side so it's not partially hiden
yueg81a77ff2017-12-05 10:29:03 -0800587 View primaryIcon = viewHolder.getPrimaryIcon();
yueg87111362017-12-08 12:45:50 -0800588 primaryIcon.animate().translationX(onRight ? -primaryIconMoveDistance : 0).start();
yueg81a77ff2017-12-05 10:29:03 -0800589 }
590
Eric Erfanian938468d2017-10-24 14:05:52 -0700591 LayoutParams getWindowParams() {
592 return windowParams;
593 }
594
595 View getRootView() {
596 return viewHolder.getRoot();
597 }
598
599 /**
600 * Hide the bubble if visible. Will run a short exit animation and before hiding, and {@code
601 * afterHiding} after hiding. If the bubble is currently showing text, will hide after the text is
602 * done displaying. If the bubble is not visible this method does nothing.
603 */
yueg87111362017-12-08 12:45:50 -0800604 @VisibleForTesting
605 void hideHelper(Runnable afterHiding) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700606 if (visibility == Visibility.HIDDEN || visibility == Visibility.EXITING) {
607 return;
608 }
609
610 // Make bubble non clickable to prevent further buggy actions
611 viewHolder.setChildClickable(false);
612
yueg349ac602018-01-17 09:02:45 -0800613 if (visibility == Visibility.ENTERING) {
614 enterAnimatorSet.removeAllListeners();
615 enterAnimatorSet.cancel();
616 enterAnimatorSet = null;
617 afterHiding.run();
618 return;
619 }
620
yueg6518fdb2018-01-09 17:30:36 -0800621 if (collapseAnimatorSet != null) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700622 collapseEndAction = CollapseEnd.HIDE;
623 return;
624 }
625
626 if (expanded) {
yuegf539f782017-12-18 16:20:58 -0800627 startCollapse(CollapseEnd.HIDE, false /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700628 return;
629 }
630
631 visibility = Visibility.EXITING;
yueg87111362017-12-08 12:45:50 -0800632
633 // Hide bubble animation: scale the whole bubble to 0, and change avatar+icon's alpha to 0
634 ObjectAnimator scaleXAnimator =
635 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleX", 0);
636 ObjectAnimator scaleYAnimator =
637 ObjectAnimator.ofFloat(viewHolder.getPrimaryButton(), "scaleY", 0);
638 ObjectAnimator avatarAlphaAnimator =
639 ObjectAnimator.ofFloat(viewHolder.getPrimaryAvatar(), "alpha", 0);
640 ObjectAnimator iconAlphaAnimator =
641 ObjectAnimator.ofFloat(viewHolder.getPrimaryIcon(), "alpha", 0);
642 exitAnimatorSet = new AnimatorSet();
643 exitAnimatorSet.playTogether(
644 scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator);
645 exitAnimatorSet.setInterpolator(new AnticipateInterpolator());
yueg6518fdb2018-01-09 17:30:36 -0800646 exitAnimatorSet.setDuration(HIDE_BUBBLE_ANIMATION_DURATION);
yueg87111362017-12-08 12:45:50 -0800647 exitAnimatorSet.addListener(
648 new AnimatorListenerAdapter() {
649 @Override
yuega235e132017-12-13 14:13:57 -0800650 public void onAnimationStart(Animator animation) {
651 viewHolder.getPrimaryButton().setAccessibilityDelegate(null);
652 }
653
654 @Override
yueg87111362017-12-08 12:45:50 -0800655 public void onAnimationEnd(Animator animation) {
656 afterHiding.run();
657 }
658 });
659 exitAnimatorSet.start();
Eric Erfanian938468d2017-10-24 14:05:52 -0700660 }
661
662 private void reset() {
663 viewHolder = new ViewHolder(viewHolder.getRoot().getContext());
664 update();
665 }
666
667 private void update() {
yuegbf7e8852018-01-23 10:00:15 -0800668 // The value may change on display size changed.
669 primaryIconMoveDistance =
670 context.getResources().getDimensionPixelSize(R.dimen.bubble_size)
671 - context.getResources().getDimensionPixelSize(R.dimen.bubble_small_icon_size);
672 // Set boundary for primary button to show elevation (background is transparent)
673 viewHolder
674 .getPrimaryButton()
675 .setOutlineProvider(
676 new ViewOutlineProvider() {
677 @Override
678 public void getOutline(View view, Outline outline) {
679 ViewOutlineProvider.BACKGROUND.getOutline(view, outline);
680 outline.setAlpha(1);
681 }
682 });
683
yuega5a08d82017-10-31 14:11:53 -0700684 // Small icon
yueg84ac49b2017-11-01 16:22:28 -0700685 Drawable smallIconBackgroundCircle =
686 context
687 .getResources()
688 .getDrawable(R.drawable.bubble_shape_circle_small, context.getTheme());
689 smallIconBackgroundCircle.setTint(context.getColor(R.color.bubble_button_color_blue));
690 viewHolder.getPrimaryIcon().setBackground(smallIconBackgroundCircle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700691 viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon());
yuega5a08d82017-10-31 14:11:53 -0700692 viewHolder.getPrimaryAvatar().setImageDrawable(currentInfo.getAvatar());
Eric Erfanian938468d2017-10-24 14:05:52 -0700693
yuega5a08d82017-10-31 14:11:53 -0700694 updatePrimaryIconAnimation();
Eric Erfanian938468d2017-10-24 14:05:52 -0700695 updateButtonStates();
696 }
697
698 private void updatePrimaryIconAnimation() {
699 Drawable drawable = viewHolder.getPrimaryIcon().getDrawable();
700 if (drawable instanceof Animatable) {
701 if (isVisible()) {
702 ((Animatable) drawable).start();
703 } else {
704 ((Animatable) drawable).stop();
705 }
706 }
707 }
708
709 private void updateButtonStates() {
yueg84ac49b2017-11-01 16:22:28 -0700710 configureButton(currentInfo.getActions().get(0), viewHolder.getFullScreenButton());
711 configureButton(currentInfo.getActions().get(1), viewHolder.getMuteButton());
712 configureButton(currentInfo.getActions().get(2), viewHolder.getAudioRouteButton());
713 configureButton(currentInfo.getActions().get(3), viewHolder.getEndCallButton());
Eric Erfanian938468d2017-10-24 14:05:52 -0700714 }
715
yueg84ac49b2017-11-01 16:22:28 -0700716 private void configureButton(Action action, NewCheckableButton button) {
yueg07e323c2017-12-19 16:05:47 -0800717 boolean isRtl =
718 TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
719 if (isRtl) {
720 button.setCompoundDrawablesWithIntrinsicBounds(null, null, action.getIconDrawable(), null);
721 } else {
722 button.setCompoundDrawablesWithIntrinsicBounds(action.getIconDrawable(), null, null, null);
723 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700724 button.setChecked(action.isChecked());
725 button.setEnabled(action.isEnabled());
yueg84ac49b2017-11-01 16:22:28 -0700726 button.setText(action.getName());
yuega235e132017-12-13 14:13:57 -0800727 button.setContentDescription(action.getName());
Eric Erfanian938468d2017-10-24 14:05:52 -0700728 button.setOnClickListener(v -> doAction(action));
729 }
730
731 private void doAction(Action action) {
732 try {
733 action.getIntent().send();
734 } catch (CanceledException e) {
735 throw new RuntimeException(e);
736 }
737 }
738
yueg81a77ff2017-12-05 10:29:03 -0800739 /**
740 * Create a new ViewHolder object to replace the old one.It only happens when not moving and
741 * collapsed.
742 */
743 void replaceViewHolder() {
744 LogUtil.enterBlock("NewBubble.replaceViewHolder");
yuegf473e1d2018-01-02 16:23:14 -0800745 // Don't do it. If windowParams is null, either we haven't initialized it or we set it to null.
746 // There is no need to recreate bubble.
747 if (windowParams == null) {
748 return;
749 }
750
Eric Erfanian938468d2017-10-24 14:05:52 -0700751 ViewHolder oldViewHolder = viewHolder;
Eric Erfanian938468d2017-10-24 14:05:52 -0700752
yueg81a77ff2017-12-05 10:29:03 -0800753 // Create a new ViewHolder and copy needed info.
754 viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext());
yueg87111362017-12-08 12:45:50 -0800755 viewHolder.getPrimaryIcon().setX(isDrawingFromRight() ? 0 : primaryIconMoveDistance);
yueg81a77ff2017-12-05 10:29:03 -0800756 viewHolder
757 .getPrimaryIcon()
yueg87111362017-12-08 12:45:50 -0800758 .setTranslationX(isDrawingFromRight() ? -primaryIconMoveDistance : 0);
yuega235e132017-12-13 14:13:57 -0800759 setPrimaryButtonAccessibilityAction(
760 context.getString(R.string.a11y_bubble_primary_button_expand_action));
Eric Erfanian938468d2017-10-24 14:05:52 -0700761
yueg81a77ff2017-12-05 10:29:03 -0800762 update();
763
764 // Add new view at its horizontal boundary
Eric Erfanian938468d2017-10-24 14:05:52 -0700765 ViewGroup root = viewHolder.getRoot();
yueg81a77ff2017-12-05 10:29:03 -0800766 windowParams.x = leftBoundary;
767 windowParams.gravity = Gravity.TOP | (isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT);
Eric Erfanian938468d2017-10-24 14:05:52 -0700768 windowManager.addView(root, windowParams);
yueg81a77ff2017-12-05 10:29:03 -0800769
770 // Remove the old view after delay
Eric Erfanian938468d2017-10-24 14:05:52 -0700771 root.getViewTreeObserver()
772 .addOnPreDrawListener(
773 new OnPreDrawListener() {
774 @Override
775 public boolean onPreDraw() {
776 root.getViewTreeObserver().removeOnPreDrawListener(this);
777 // Wait a bit before removing the old view; make sure the new one has drawn over it.
778 handler.postDelayed(
779 () -> windowManager.removeView(oldViewHolder.getRoot()),
780 WINDOW_REDRAW_DELAY_MILLIS);
781 return true;
782 }
783 });
784 }
785
yueg81a77ff2017-12-05 10:29:03 -0800786 int getDrawerVisibility() {
787 return viewHolder.getExpandedView().getVisibility();
Eric Erfanian938468d2017-10-24 14:05:52 -0700788 }
789
yuegf473e1d2018-01-02 16:23:14 -0800790 void bottomActionDismiss() {
791 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_DISMISS);
792 // Create bubble at default location at next time
793 hideAndReset();
794 windowParams = null;
795 }
796
797 void bottomActionEndCall() {
798 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_END_CALL);
yueg6518fdb2018-01-09 17:30:36 -0800799 DialerCall call = getCall();
800 if (call != null) {
801 call.disconnect();
802 }
yuegf473e1d2018-01-02 16:23:14 -0800803 }
804
Eric Erfanian938468d2017-10-24 14:05:52 -0700805 private boolean isDrawingFromRight() {
806 return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
807 }
808
809 private void setFocused(boolean focused) {
810 if (focused) {
811 windowParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
812 } else {
813 windowParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
814 }
815 windowManager.updateViewLayout(getRootView(), windowParams);
816 }
817
818 private void defaultAfterHidingAnimation() {
yueg87111362017-12-08 12:45:50 -0800819 exitAnimatorSet = null;
820 viewHolder.getPrimaryButton().setVisibility(View.INVISIBLE);
Eric Erfanian938468d2017-10-24 14:05:52 -0700821 windowManager.removeView(viewHolder.getRoot());
822 visibility = Visibility.HIDDEN;
823
824 updatePrimaryIconAnimation();
825 }
826
yueg81a77ff2017-12-05 10:29:03 -0800827 private void logBasicOrCallImpression(DialerImpression.Type impressionType) {
yuegf473e1d2018-01-02 16:23:14 -0800828 DialerCall call = getCall();
yueg81a77ff2017-12-05 10:29:03 -0800829 if (call != null) {
830 Logger.get(context)
831 .logCallImpression(impressionType, call.getUniqueCallId(), call.getTimeAddedMs());
832 } else {
833 Logger.get(context).logImpression(impressionType);
834 }
835 }
836
yuegf473e1d2018-01-02 16:23:14 -0800837 private DialerCall getCall() {
838 // Bubble is shown for outgoing, active or background call
839 DialerCall call = CallList.getInstance().getOutgoingCall();
840 if (call == null) {
841 call = CallList.getInstance().getActiveOrBackgroundCall();
842 }
843 return call;
844 }
845
yuega235e132017-12-13 14:13:57 -0800846 private void setPrimaryButtonAccessibilityAction(String description) {
847 viewHolder
848 .getPrimaryButton()
849 .setAccessibilityDelegate(
850 new AccessibilityDelegate() {
851 @Override
852 public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) {
853 super.onInitializeAccessibilityNodeInfo(v, info);
854
855 AccessibilityAction clickAction =
856 new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, description);
857 info.addAction(clickAction);
858 }
859 });
860 }
861
yueg0b4755c2017-12-18 10:01:03 -0800862 private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider(View view) {
863 int startRectX = isDrawingFromRight() ? view.getMeasuredWidth() : 0;
864 Rect startRect = new Rect(startRectX, 0, startRectX, 0);
865 Rect endRect = new Rect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
866
867 float bubbleRadius = context.getResources().getDimension(R.dimen.bubble_radius);
868 return new RoundedRectRevealOutlineProvider(bubbleRadius, bubbleRadius, startRect, endRect);
869 }
870
871 private ValueAnimator createBubbleMoveAnimator(int startX, int endX, int startY, float k) {
872 ValueAnimator xValueAnimator = ValueAnimator.ofFloat(startX, endX);
873 xValueAnimator.setInterpolator(new LinearOutSlowInInterpolator());
874 xValueAnimator.addUpdateListener(
875 (valueAnimator) -> {
876 // Update windowParams and the root layout.
877 // We can't do ViewPropertyAnimation since it clips children.
878 float newX = (float) valueAnimator.getAnimatedValue();
879 if (k != 0) {
880 windowParams.y = startY + (int) (Math.abs(newX - (float) startX) * k);
881 }
882 windowParams.x = (int) newX;
883 windowManager.updateViewLayout(viewHolder.getRoot(), windowParams);
884 });
885 return xValueAnimator;
886 }
887
Eric Erfanian938468d2017-10-24 14:05:52 -0700888 @VisibleForTesting
889 class ViewHolder {
890
yueg0b4755c2017-12-18 10:01:03 -0800891 private NewMoveHandler moveHandler;
Eric Erfanian938468d2017-10-24 14:05:52 -0700892 private final NewWindowRoot root;
yueg7e421822018-01-19 11:39:03 -0800893 private final View primaryButton;
Eric Erfanian938468d2017-10-24 14:05:52 -0700894 private final ImageView primaryIcon;
yuega5a08d82017-10-31 14:11:53 -0700895 private final ImageView primaryAvatar;
yueg0b4755c2017-12-18 10:01:03 -0800896 private final View arrow;
Eric Erfanian938468d2017-10-24 14:05:52 -0700897
898 private final NewCheckableButton fullScreenButton;
899 private final NewCheckableButton muteButton;
900 private final NewCheckableButton audioRouteButton;
901 private final NewCheckableButton endCallButton;
902 private final View expandedView;
903
904 public ViewHolder(Context context) {
905 // Window root is not in the layout file so that the inflater has a view to inflate into
906 this.root = new NewWindowRoot(context);
907 LayoutInflater inflater = LayoutInflater.from(root.getContext());
908 View contentView = inflater.inflate(R.layout.new_bubble_base, root, true);
909 expandedView = contentView.findViewById(R.id.bubble_expanded_layout);
910 primaryButton = contentView.findViewById(R.id.bubble_button_primary);
yuega5a08d82017-10-31 14:11:53 -0700911 primaryAvatar = contentView.findViewById(R.id.bubble_icon_avatar);
Eric Erfanian938468d2017-10-24 14:05:52 -0700912 primaryIcon = contentView.findViewById(R.id.bubble_icon_primary);
yueg0b4755c2017-12-18 10:01:03 -0800913 arrow = contentView.findViewById(R.id.bubble_triangle);
Eric Erfanian938468d2017-10-24 14:05:52 -0700914
915 fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen);
916 muteButton = contentView.findViewById(R.id.bubble_button_mute);
917 audioRouteButton = contentView.findViewById(R.id.bubble_button_audio_route);
918 endCallButton = contentView.findViewById(R.id.bubble_button_end_call);
919
920 root.setOnBackPressedListener(
921 () -> {
922 if (visibility == Visibility.SHOWING && expanded) {
yuegf539f782017-12-18 16:20:58 -0800923 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
924 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700925 return true;
926 }
927 return false;
928 });
929 root.setOnConfigurationChangedListener(
930 (configuration) -> {
yueg6518fdb2018-01-09 17:30:36 -0800931 if (expanded) {
yueg10f6e822018-01-17 15:32:18 -0800932 startCollapse(CollapseEnd.NOTHING, false /* shouldRecoverYPosition */);
yueg6518fdb2018-01-09 17:30:36 -0800933 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700934 // The values in the current MoveHandler may be stale, so replace it. Then ensure the
yuegbf7e8852018-01-23 10:00:15 -0800935 // Window is in bounds, and redraw the changes
Eric Erfanian938468d2017-10-24 14:05:52 -0700936 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
937 moveHandler.snapToBounds();
yuegbf7e8852018-01-23 10:00:15 -0800938 replaceViewHolder();
Eric Erfanian938468d2017-10-24 14:05:52 -0700939 });
940 root.setOnTouchListener(
941 (v, event) -> {
942 if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
yuegf539f782017-12-18 16:20:58 -0800943 logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE);
944 startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */);
Eric Erfanian938468d2017-10-24 14:05:52 -0700945 return true;
946 }
947 return false;
948 });
949 moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
950 }
951
952 private void setChildClickable(boolean clickable) {
953 fullScreenButton.setClickable(clickable);
954 muteButton.setClickable(clickable);
955 audioRouteButton.setClickable(clickable);
956 endCallButton.setClickable(clickable);
yueg0b4755c2017-12-18 10:01:03 -0800957 setPrimaryButtonClickable(clickable);
958 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700959
yueg0b4755c2017-12-18 10:01:03 -0800960 private void setPrimaryButtonClickable(boolean clickable) {
Eric Erfanian938468d2017-10-24 14:05:52 -0700961 moveHandler.setClickable(clickable);
962 }
963
yueg81a77ff2017-12-05 10:29:03 -0800964 public int getMoveUpDistance() {
965 int deltaAllowed =
966 expandedView.getHeight()
967 - context
968 .getResources()
969 .getDimensionPixelOffset(R.dimen.bubble_button_padding_vertical)
970 * 2;
971 return moveHandler.getMoveUpDistance(deltaAllowed);
972 }
973
Eric Erfanian938468d2017-10-24 14:05:52 -0700974 public ViewGroup getRoot() {
975 return root;
976 }
977
yueg7e421822018-01-19 11:39:03 -0800978 public View getPrimaryButton() {
Eric Erfanian938468d2017-10-24 14:05:52 -0700979 return primaryButton;
980 }
981
982 public ImageView getPrimaryIcon() {
983 return primaryIcon;
984 }
985
yuega5a08d82017-10-31 14:11:53 -0700986 public ImageView getPrimaryAvatar() {
987 return primaryAvatar;
988 }
989
Eric Erfanian938468d2017-10-24 14:05:52 -0700990 public View getExpandedView() {
991 return expandedView;
992 }
993
yueg0b4755c2017-12-18 10:01:03 -0800994 public View getArrow() {
995 return arrow;
996 }
997
Eric Erfanian938468d2017-10-24 14:05:52 -0700998 public NewCheckableButton getFullScreenButton() {
999 return fullScreenButton;
1000 }
1001
1002 public NewCheckableButton getMuteButton() {
1003 return muteButton;
1004 }
1005
1006 public NewCheckableButton getAudioRouteButton() {
1007 return audioRouteButton;
1008 }
1009
1010 public NewCheckableButton getEndCallButton() {
1011 return endCallButton;
1012 }
1013
1014 public void setDrawerVisibility(int visibility) {
1015 expandedView.setVisibility(visibility);
1016 }
1017
1018 public boolean isMoving() {
1019 return moveHandler.isMoving();
1020 }
1021
1022 public void undoGravityOverride() {
1023 moveHandler.undoGravityOverride();
1024 }
1025 }
Eric Erfanian938468d2017-10-24 14:05:52 -07001026}