Bubble v2 dismiss.

Drag and drop bubble to bottom to hide or end call. Flinging to bottom does not trigger the actions. Color/text is not final. Navigation bar is not hiden and the change will be in a following CL.

Bug: 67605985
Test: NewBubbleTest
PiperOrigin-RevId: 180608133
Change-Id: Iff4cb32226d8fbf0f8e5319f6876a1d74c336b4a
diff --git a/java/com/android/dialer/logging/dialer_impression.proto b/java/com/android/dialer/logging/dialer_impression.proto
index 2d2eebf..a17d365 100644
--- a/java/com/android/dialer/logging/dialer_impression.proto
+++ b/java/com/android/dialer/logging/dialer_impression.proto
@@ -12,7 +12,7 @@
   // Event enums to be used for Impression Logging in Dialer.
   // It's perfectly acceptable for this enum to be large
   // Values should be from 1000 to 100000.
-  // Next Tag: 1320
+  // Next Tag: 1322
   enum Type {
     UNKNOWN_AOSP_EVENT_TYPE = 1000;
 
@@ -644,5 +644,9 @@
     BUBBLE_V2_BLUETOOTH = 1318;
     // User ended call from bubble call action menu
     BUBBLE_V2_END_CALL = 1319;
+    // Drag bubble to bottom and dismiss
+    BUBBLE_V2_BOTTOM_ACTION_DISMISS = 1320;
+    // Drag bubble to bottom and end call
+    BUBBLE_V2_BOTTOM_ACTION_END_CALL = 1321;
   }
 }
diff --git a/java/com/android/newbubble/BottomActionViewController.java b/java/com/android/newbubble/BottomActionViewController.java
new file mode 100644
index 0000000..7c71051
--- /dev/null
+++ b/java/com/android/newbubble/BottomActionViewController.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.newbubble;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.support.v4.os.BuildCompat;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.view.animation.LinearInterpolator;
+
+/** Controller for showing and hiding bubble bottom action view. */
+final class BottomActionViewController {
+
+  // This delay controls how long to wait before we show the target when the user first moves
+  // the bubble, to prevent the bottom action view from animating if the user just wants to fling
+  // the bubble.
+  private static final int SHOW_TARGET_DELAY = 100;
+  private static final int SHOW_TARGET_DURATION = 350;
+  private static final int HIDE_TARGET_DURATION = 225;
+  private static final float HIGHLIGHT_TARGET_SCALE = 1.5f;
+
+  private final Context context;
+  private final WindowManager windowManager;
+  private final int gradientHeight;
+  private final int bottomActionViewTop;
+
+  private View bottomActionView;
+  private View dismissView;
+  private View endCallView;
+
+  private boolean dismissHighlighted;
+  private boolean endCallHighlighted;
+
+  public BottomActionViewController(Context context) {
+    this.context = context;
+    windowManager = context.getSystemService(WindowManager.class);
+    gradientHeight =
+        context.getResources().getDimensionPixelSize(R.dimen.bubble_bottom_action_view_height);
+    bottomActionViewTop = context.getResources().getDisplayMetrics().heightPixels - gradientHeight;
+  }
+
+  /** Creates and show the bottom action view. */
+  public void createAndShowBottomActionView() {
+    if (bottomActionView != null) {
+      return;
+    }
+
+    // Create a new view for the dismiss target
+    bottomActionView = LayoutInflater.from(context).inflate(R.layout.bottom_action_base, null);
+    bottomActionView.setAlpha(0);
+
+    // Sub views
+    dismissView = bottomActionView.findViewById(R.id.bottom_action_dismiss_layout);
+    endCallView = bottomActionView.findViewById(R.id.bottom_action_end_call_layout);
+
+    // Add the target to the window
+    // TODO(yueg): use TYPE_NAVIGATION_BAR_PANEL to draw over navigation bar
+    LayoutParams layoutParams =
+        new LayoutParams(
+            LayoutParams.MATCH_PARENT,
+            gradientHeight,
+            0,
+            bottomActionViewTop,
+            BuildCompat.isAtLeastO()
+                ? LayoutParams.TYPE_APPLICATION_OVERLAY
+                : LayoutParams.TYPE_SYSTEM_OVERLAY,
+            LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                | LayoutParams.FLAG_NOT_TOUCHABLE
+                | LayoutParams.FLAG_NOT_FOCUSABLE,
+            PixelFormat.TRANSLUCENT);
+    layoutParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
+    windowManager.addView(bottomActionView, layoutParams);
+    bottomActionView.setSystemUiVisibility(
+        View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+            | View.SYSTEM_UI_FLAG_FULLSCREEN);
+    bottomActionView
+        .getRootView()
+        .setSystemUiVisibility(
+            View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+                | View.SYSTEM_UI_FLAG_FULLSCREEN);
+
+    // Shows the botton action view
+    bottomActionView
+        .animate()
+        .alpha(1f)
+        .setInterpolator(new LinearInterpolator())
+        .setStartDelay(SHOW_TARGET_DELAY)
+        .setDuration(SHOW_TARGET_DURATION)
+        .start();
+  }
+
+  /** Hides and destroys the bottom action view. */
+  public void destroyBottomActionView() {
+    if (bottomActionView == null) {
+      return;
+    }
+    bottomActionView
+        .animate()
+        .alpha(0f)
+        .setInterpolator(new LinearInterpolator())
+        .setDuration(HIDE_TARGET_DURATION)
+        .withEndAction(
+            () -> {
+              // Use removeViewImmediate instead of removeView to avoid view flashing before removed
+              windowManager.removeViewImmediate(bottomActionView);
+              bottomActionView = null;
+            })
+        .start();
+  }
+
+  /**
+   * Change highlight state of dismiss view and end call view according to current touch point.
+   * Highlight the view with touch point moving into its boundary. Unhighlight the view with touch
+   * point moving out of its boundary.
+   *
+   * @param x x position of current touch point
+   * @param y y position of current touch point
+   */
+  public void highlightIfHover(float x, float y) {
+    if (bottomActionView == null) {
+      return;
+    }
+    final int middle = context.getResources().getDisplayMetrics().widthPixels / 2;
+    boolean shouldHighlightDismiss = y > bottomActionViewTop && x < middle;
+    boolean shouldHighlightEndCall = y > bottomActionViewTop && x >= middle;
+
+    if (!shouldHighlightDismiss && dismissHighlighted) {
+      // Unhighlight dismiss
+      dismissView.animate().scaleX(1f).scaleY(1f).setDuration(HIDE_TARGET_DURATION).start();
+      dismissHighlighted = false;
+    } else if (!shouldHighlightEndCall && endCallHighlighted) {
+      // Unhighlight end call
+      endCallView.animate().scaleX(1f).scaleY(1f).setDuration(HIDE_TARGET_DURATION).start();
+      endCallHighlighted = false;
+    }
+
+    if (shouldHighlightDismiss && !dismissHighlighted) {
+      // Highlight dismiss
+      dismissView
+          .animate()
+          .scaleX(HIGHLIGHT_TARGET_SCALE)
+          .scaleY(HIGHLIGHT_TARGET_SCALE)
+          .setDuration(SHOW_TARGET_DURATION)
+          .start();
+      dismissHighlighted = true;
+    } else if (shouldHighlightEndCall && !endCallHighlighted) {
+      // Highlight end call
+      endCallView
+          .animate()
+          .scaleX(HIGHLIGHT_TARGET_SCALE)
+          .scaleY(HIGHLIGHT_TARGET_SCALE)
+          .setDuration(SHOW_TARGET_DURATION)
+          .start();
+      endCallHighlighted = true;
+    }
+  }
+
+  public boolean isDismissHighlighted() {
+    return dismissHighlighted;
+  }
+
+  public boolean isEndCallHighlighted() {
+    return endCallHighlighted;
+  }
+}
diff --git a/java/com/android/newbubble/NewBubble.java b/java/com/android/newbubble/NewBubble.java
index 469c15d..f5a036f 100644
--- a/java/com/android/newbubble/NewBubble.java
+++ b/java/com/android/newbubble/NewBubble.java
@@ -60,6 +60,7 @@
 import android.view.animation.OvershootInterpolator;
 import android.widget.ImageView;
 import android.widget.TextView;
+import android.widget.Toast;
 import android.widget.ViewAnimator;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.logging.DialerImpression;
@@ -830,6 +831,12 @@
    */
   void replaceViewHolder() {
     LogUtil.enterBlock("NewBubble.replaceViewHolder");
+    // Don't do it. If windowParams is null, either we haven't initialized it or we set it to null.
+    // There is no need to recreate bubble.
+    if (windowParams == null) {
+      return;
+    }
+
     ViewHolder oldViewHolder = viewHolder;
 
     // Create a new ViewHolder and copy needed info.
@@ -873,6 +880,27 @@
     return viewHolder.getExpandedView().getVisibility();
   }
 
+  void bottomActionDismiss() {
+    logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_DISMISS);
+    // Create bubble at default location at next time
+    hideAndReset();
+    windowParams = null;
+  }
+
+  void bottomActionEndCall() {
+    logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_END_CALL);
+    // Hide without animation
+    hideHelper(
+        () -> {
+          defaultAfterHidingAnimation();
+          DialerCall call = getCall();
+          if (call != null) {
+            call.disconnect();
+            Toast.makeText(context, R.string.incall_call_ended, Toast.LENGTH_SHORT).show();
+          }
+        });
+  }
+
   private boolean isDrawingFromRight() {
     return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
   }
@@ -896,11 +924,7 @@
   }
 
   private void logBasicOrCallImpression(DialerImpression.Type impressionType) {
-    // Bubble is shown for outgoing, active or background call
-    DialerCall call = CallList.getInstance().getOutgoingCall();
-    if (call == null) {
-      call = CallList.getInstance().getActiveOrBackgroundCall();
-    }
+    DialerCall call = getCall();
     if (call != null) {
       Logger.get(context)
           .logCallImpression(impressionType, call.getUniqueCallId(), call.getTimeAddedMs());
@@ -909,6 +933,15 @@
     }
   }
 
+  private DialerCall getCall() {
+    // Bubble is shown for outgoing, active or background call
+    DialerCall call = CallList.getInstance().getOutgoingCall();
+    if (call == null) {
+      call = CallList.getInstance().getActiveOrBackgroundCall();
+    }
+    return call;
+  }
+
   private void setPrimaryButtonAccessibilityAction(String description) {
     viewHolder
         .getPrimaryButton()
diff --git a/java/com/android/newbubble/NewMoveHandler.java b/java/com/android/newbubble/NewMoveHandler.java
index 9e6d955..c00c107 100644
--- a/java/com/android/newbubble/NewMoveHandler.java
+++ b/java/com/android/newbubble/NewMoveHandler.java
@@ -51,6 +51,7 @@
   private final int bubbleShadowPaddingHorizontal;
   private final int bubbleExpandedViewWidth;
   private final float touchSlopSquared;
+  private final BottomActionViewController bottomActionViewController;
 
   private boolean clickable = true;
   private boolean isMoving;
@@ -156,6 +157,8 @@
     // efficient than needing to take a square root.
     touchSlopSquared = (float) Math.pow(ViewConfiguration.get(context).getScaledTouchSlop(), 2);
 
+    bottomActionViewController = new BottomActionViewController(context);
+
     targetView.setOnTouchListener(this);
   }
 
@@ -200,7 +203,9 @@
           if (!isMoving) {
             isMoving = true;
             bubble.onMoveStart();
+            bottomActionViewController.createAndShowBottomActionView();
           }
+          bottomActionViewController.highlightIfHover(eventX, eventY);
 
           ensureSprings();
 
@@ -229,11 +234,16 @@
 
             moveXAnimation.animateToFinalPosition(target.x);
             moveYAnimation.animateToFinalPosition(target.y);
+          } else if (bottomActionViewController.isDismissHighlighted()) {
+            bubble.bottomActionDismiss();
+          } else if (bottomActionViewController.isEndCallHighlighted()) {
+            bubble.bottomActionEndCall();
           } else {
             snapX();
           }
           isMoving = false;
           bubble.onMoveFinish();
+          bottomActionViewController.destroyBottomActionView();
         } else {
           v.performClick();
           if (clickable) {
diff --git a/java/com/android/newbubble/res/drawable/bottom_action_scrim.xml b/java/com/android/newbubble/res/drawable/bottom_action_scrim.xml
new file mode 100644
index 0000000..bd13382
--- /dev/null
+++ b/java/com/android/newbubble/res/drawable/bottom_action_scrim.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+  <gradient
+      android:angle="90"
+      android:endColor="@android:color/transparent"
+      android:startColor="#AA000000"/>
+</shape>
diff --git a/java/com/android/newbubble/res/layout/bottom_action_base.xml b/java/com/android/newbubble/res/layout/bottom_action_base.xml
new file mode 100644
index 0000000..bf08e1b
--- /dev/null
+++ b/java/com/android/newbubble/res/layout/bottom_action_base.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/bubble_bottom_action_view_height"
+    android:orientation="horizontal"
+    android:background="@drawable/bottom_action_scrim">
+
+  <LinearLayout
+      android:id="@+id/bottom_action_dismiss_layout"
+      android:layout_width="0dp"
+      android:layout_height="match_parent"
+      android:layout_weight="1"
+      android:gravity="center_horizontal|center_vertical">
+    <ImageView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/bubble_button_icon_padding"
+        android:src="@drawable/quantum_ic_clear_vd_theme_24"
+        android:tint="@color/bubble_button_color_white"/>
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAllCaps="true"
+        android:textColor="@color/bubble_button_color_white"
+        android:text="Hide"/>
+  </LinearLayout>
+
+  <LinearLayout
+      android:id="@+id/bottom_action_end_call_layout"
+      android:layout_width="0dp"
+      android:layout_height="match_parent"
+      android:layout_weight="1"
+      android:gravity="center_horizontal|center_vertical">
+    <ImageView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/bubble_button_icon_padding"
+        android:src="@drawable/quantum_ic_call_end_vd_theme_24"
+        android:tint="@color/bubble_button_color_white"/>
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAllCaps="true"
+        android:textColor="@color/bubble_button_color_white"
+        android:text="End call"/>
+  </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/java/com/android/newbubble/res/values/values.xml b/java/com/android/newbubble/res/values/values.xml
index 71f813a..040a5be 100644
--- a/java/com/android/newbubble/res/values/values.xml
+++ b/java/com/android/newbubble/res/values/values.xml
@@ -41,4 +41,6 @@
   <dimen name="bubble_expanded_separator_height">8dp</dimen>
   <dimen name="bubble_small_icon_size">24dp</dimen>
   <dimen name="bubble_small_icon_padding">4dp</dimen>
+
+  <dimen name="bubble_bottom_action_view_height">180dp</dimen>
 </resources>