Merge changes I05dc2ddd,Ic6bfd4ab,I58557f77

* changes:
  Added tests for latin smart dial map.
  Upgrade target SDK version to 27.
  Bubble v2 animation change.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d1585e0..b459a44 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -21,7 +21,7 @@
 
   <uses-sdk
     android:minSdkVersion="23"
-    android:targetSdkVersion="26"/>
+    android:targetSdkVersion="27"/>
 
   <uses-permission android:name="android.permission.CALL_PHONE"/>
   <uses-permission android:name="android.permission.READ_CONTACTS"/>
diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml
index 12abaa6..3652c95 100644
--- a/java/com/android/dialer/app/AndroidManifest.xml
+++ b/java/com/android/dialer/app/AndroidManifest.xml
@@ -56,7 +56,7 @@
 
   <uses-sdk
     android:minSdkVersion="23"
-    android:targetSdkVersion="26"/>
+    android:targetSdkVersion="27"/>
 
   <application android:theme="@style/Theme.AppCompat">
 
diff --git a/java/com/android/dialer/assisteddialing/AndroidManifest.xml b/java/com/android/dialer/assisteddialing/AndroidManifest.xml
index 6625dff..a19af3b 100644
--- a/java/com/android/dialer/assisteddialing/AndroidManifest.xml
+++ b/java/com/android/dialer/assisteddialing/AndroidManifest.xml
@@ -17,6 +17,6 @@
 
   <uses-sdk
       android:minSdkVersion="23"
-      android:targetSdkVersion="24"/>
+      android:targetSdkVersion="27"/>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/java/com/android/dialer/assisteddialing/ui/AndroidManifest.xml b/java/com/android/dialer/assisteddialing/ui/AndroidManifest.xml
index 5266b13..3e79de3 100644
--- a/java/com/android/dialer/assisteddialing/ui/AndroidManifest.xml
+++ b/java/com/android/dialer/assisteddialing/ui/AndroidManifest.xml
@@ -17,7 +17,7 @@
 
   <uses-sdk
       android:minSdkVersion="23"
-      android:targetSdkVersion="24"/>
+      android:targetSdkVersion="27"/>
 
   <application>
     <activity
@@ -32,4 +32,4 @@
     </activity>
   </application>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/java/com/android/dialer/binary/google/AndroidManifest.xml b/java/com/android/dialer/binary/google/AndroidManifest.xml
index 018c2a7..86f6bcb 100644
--- a/java/com/android/dialer/binary/google/AndroidManifest.xml
+++ b/java/com/android/dialer/binary/google/AndroidManifest.xml
@@ -21,7 +21,7 @@
 
   <uses-sdk
     android:minSdkVersion="23"
-    android:targetSdkVersion="26"/>
+    android:targetSdkVersion="27"/>
 
   <uses-permission android:name="android.permission.CALL_PHONE"/>
   <uses-permission android:name="android.permission.READ_CONTACTS"/>
diff --git a/java/com/android/dialer/shortcuts/AndroidManifest.xml b/java/com/android/dialer/shortcuts/AndroidManifest.xml
index 15f7794..1170058 100644
--- a/java/com/android/dialer/shortcuts/AndroidManifest.xml
+++ b/java/com/android/dialer/shortcuts/AndroidManifest.xml
@@ -18,7 +18,7 @@
 
   <uses-sdk
     android:minSdkVersion="23"
-    android:targetSdkVersion="26"/>
+    android:targetSdkVersion="27"/>
 
   <application android:theme="@style/Theme.AppCompat">
 
diff --git a/java/com/android/dialer/smartdial/LatinSmartDialMap.java b/java/com/android/dialer/smartdial/LatinSmartDialMap.java
index c512c5d..656fd12 100644
--- a/java/com/android/dialer/smartdial/LatinSmartDialMap.java
+++ b/java/com/android/dialer/smartdial/LatinSmartDialMap.java
@@ -16,6 +16,7 @@
 
 package com.android.dialer.smartdial;
 
+/** {@link SmartDialMap} for Latin based T9 dialpad searching. */
 public class LatinSmartDialMap implements SmartDialMap {
 
   private static final char[] LATIN_LETTERS_TO_DIGITS = {
diff --git a/java/com/android/incallui/AndroidManifest.xml b/java/com/android/incallui/AndroidManifest.xml
index d854a7f..af99700 100644
--- a/java/com/android/incallui/AndroidManifest.xml
+++ b/java/com/android/incallui/AndroidManifest.xml
@@ -19,7 +19,7 @@
 
   <uses-sdk
       android:minSdkVersion="23"
-      android:targetSdkVersion="26"/>
+      android:targetSdkVersion="27"/>
 
   <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
   <!-- We use this to disable the status bar buttons of home, back and recent
diff --git a/java/com/android/incallui/NewReturnToCallController.java b/java/com/android/incallui/NewReturnToCallController.java
index 95da1c6..d3d9930 100644
--- a/java/com/android/incallui/NewReturnToCallController.java
+++ b/java/com/android/incallui/NewReturnToCallController.java
@@ -219,11 +219,15 @@
   }
 
   private void onPhotoAvatarReceived(@NonNull Drawable photo) {
-    bubble.updatePhotoAvatar(photo);
+    if (bubble != null) {
+      bubble.updatePhotoAvatar(photo);
+    }
   }
 
   private void onLetterTileAvatarReceived(@NonNull Drawable photo) {
-    bubble.updateAvatar(photo);
+    if (bubble != null) {
+      bubble.updateAvatar(photo);
+    }
   }
 
   private NewBubbleInfo generateBubbleInfo() {
diff --git a/java/com/android/incallui/autoresizetext/AndroidManifest.xml b/java/com/android/incallui/autoresizetext/AndroidManifest.xml
index 1b5c193..e26670e 100644
--- a/java/com/android/incallui/autoresizetext/AndroidManifest.xml
+++ b/java/com/android/incallui/autoresizetext/AndroidManifest.xml
@@ -19,7 +19,7 @@
 
   <uses-sdk
       android:minSdkVersion="23"
-      android:targetSdkVersion="26"/>
+      android:targetSdkVersion="27"/>
 
   <application />
 </manifest>
diff --git a/java/com/android/incallui/video/protocol/AndroidManifest.xml b/java/com/android/incallui/video/protocol/AndroidManifest.xml
index cfb6b27..6f65582 100644
--- a/java/com/android/incallui/video/protocol/AndroidManifest.xml
+++ b/java/com/android/incallui/video/protocol/AndroidManifest.xml
@@ -18,5 +18,5 @@
     package="com.android.incallui.video.protocol">
   <uses-sdk
       android:minSdkVersion="23"
-      android:targetSdkVersion="26"/>
-</manifest>
\ No newline at end of file
+      android:targetSdkVersion="27"/>
+</manifest>
diff --git a/java/com/android/newbubble/NewBubble.java b/java/com/android/newbubble/NewBubble.java
index 34a9585..d13952b 100644
--- a/java/com/android/newbubble/NewBubble.java
+++ b/java/com/android/newbubble/NewBubble.java
@@ -25,8 +25,8 @@
 import android.app.PendingIntent.CanceledException;
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.Path;
 import android.graphics.PixelFormat;
+import android.graphics.Rect;
 import android.graphics.drawable.Animatable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
@@ -38,7 +38,6 @@
 import android.support.annotation.VisibleForTesting;
 import android.support.v4.graphics.ColorUtils;
 import android.support.v4.os.BuildCompat;
-import android.support.v4.view.animation.FastOutLinearInInterpolator;
 import android.support.v4.view.animation.LinearOutSlowInInterpolator;
 import android.transition.TransitionManager;
 import android.transition.TransitionValues;
@@ -55,6 +54,7 @@
 import android.view.WindowManager.LayoutParams;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.AnticipateInterpolator;
 import android.view.animation.OvershootInterpolator;
 import android.widget.ImageView;
@@ -87,8 +87,13 @@
   // This ensures the new window has had time to draw first.
   private static final int WINDOW_REDRAW_DELAY_MILLIS = 50;
 
+  private static final int EXPAND_AND_COLLAPSE_ANIMATION_DURATION = 200;
+
   private static Boolean canShowBubblesForTesting = null;
 
+  private final AccelerateDecelerateInterpolator accelerateDecelerateInterpolator =
+      new AccelerateDecelerateInterpolator();
+
   private final Context context;
   private final WindowManager windowManager;
 
@@ -233,7 +238,12 @@
     }
     setPrimaryButtonAccessibilityAction(
         context.getString(R.string.a11y_bubble_primary_button_collapse_action));
+
     viewHolder.setDrawerVisibility(View.INVISIBLE);
+    viewHolder.getArrow().setVisibility(View.INVISIBLE);
+    // No click during animation to avoid jank.
+    viewHolder.setPrimaryButtonClickable(false);
+
     View expandedView = viewHolder.getExpandedView();
     expandedView
         .getViewTreeObserver()
@@ -247,47 +257,46 @@
                   savedYPosition = windowParams.y;
                 }
 
-                // Calculate the move-to-middle distance
+                // Animation 1: animate x-move and y-move (if needed) together
                 int deltaX =
                     (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
-                float k = (float) moveUpDistance / deltaX;
+                float k = -(float) moveUpDistance / deltaX;
                 if (isDrawingFromRight()) {
                   deltaX = -deltaX;
                 }
+                ValueAnimator xValueAnimator =
+                    createBubbleMoveAnimator(
+                        windowParams.x - deltaX, windowParams.x, windowParams.y, k);
 
-                // Do X-move and Y-move together
+                // Show expanded view
+                expandedView.setVisibility(View.VISIBLE);
 
-                final int startX = windowParams.x - deltaX;
-                final int startY = windowParams.y;
-                ValueAnimator animator = ValueAnimator.ofFloat(startX, windowParams.x);
-                animator.setInterpolator(new LinearOutSlowInInterpolator());
-                animator.addUpdateListener(
-                    (valueAnimator) -> {
-                      // Update windowParams and the root layout.
-                      // We can't do ViewPropertyAnimation since it clips children.
-                      float newX = (float) valueAnimator.getAnimatedValue();
-                      if (moveUpDistance != 0) {
-                        windowParams.y = startY - (int) (Math.abs(newX - (float) startX) * k);
-                      }
-                      windowParams.x = (int) newX;
-                      windowManager.updateViewLayout(viewHolder.getRoot(), windowParams);
-                    });
-                animator.addListener(
+                // Animator 2: reveal expanded view from top left or top right
+                View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
+                ValueAnimator revealAnim =
+                    createOpenCloseOutlineProvider(expandedMenu)
+                        .createRevealAnimator(expandedMenu, false);
+                revealAnim.setInterpolator(accelerateDecelerateInterpolator);
+
+                // Animator 3: expanded view fade in
+                Animator fadeIn = ObjectAnimator.ofFloat(expandedView, "alpha", 0, 1);
+                fadeIn.setInterpolator(accelerateDecelerateInterpolator);
+
+                // Play all animation together
+                AnimatorSet expandAnimatorSet = new AnimatorSet();
+                expandAnimatorSet.playTogether(revealAnim, fadeIn, xValueAnimator);
+                expandAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
+                expandAnimatorSet.addListener(
                     new AnimatorListenerAdapter() {
                       @Override
                       public void onAnimationEnd(Animator animation) {
-                        // Show expanded view
-                        expandedView.setVisibility(View.VISIBLE);
-                        expandedView.setTranslationY(-expandedView.getHeight());
-                        expandedView.setAlpha(0);
-                        expandedView
-                            .animate()
-                            .setInterpolator(new LinearOutSlowInInterpolator())
-                            .translationY(0)
-                            .alpha(1);
+                        // Show arrow after animation
+                        viewHolder.getArrow().setVisibility(View.VISIBLE);
+                        // Safe to click primary button now
+                        viewHolder.setPrimaryButtonClickable(true);
                       }
                     });
-                animator.start();
+                expandAnimatorSet.start();
 
                 expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
                 return false;
@@ -315,89 +324,79 @@
     if (isUserAction && collapseEndAction == CollapseEnd.NOTHING) {
       logBasicOrCallImpression(DialerImpression.Type.BUBBLE_COLLAPSE_BY_USER);
     }
+
     setPrimaryButtonAccessibilityAction(
         context.getString(R.string.a11y_bubble_primary_button_expand_action));
-    // Animate expanded view to move from its position to above primary button and hide
-    collapseAnimation =
-        expandedView
-            .animate()
-            .translationY(-expandedView.getHeight())
-            .alpha(0)
-            .setInterpolator(new FastOutLinearInInterpolator())
-            .withEndAction(
+
+    // Hide arrow before animation
+    viewHolder.getArrow().setVisibility(View.INVISIBLE);
+
+    // No click during animation to avoid jank.
+    viewHolder.setPrimaryButtonClickable(false);
+
+    // Calculate animation values
+    int deltaX = (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
+    float k =
+        (savedYPosition != -1 && shouldRecoverYPosition)
+            ? (savedYPosition - windowParams.y) / (float) deltaX
+            : 0;
+    // The position is not useful after collapse
+    savedYPosition = -1;
+
+    // Animation 1: animate x-move and y-move (if needed) together
+    ValueAnimator xValueAnimator =
+        createBubbleMoveAnimator(windowParams.x, windowParams.x - deltaX, windowParams.y, k);
+
+    // Animator 2: hide expanded view to top left or top right
+    View expandedMenu = viewHolder.getRoot().findViewById(R.id.bubble_expanded_menu);
+    ValueAnimator revealAnim =
+        createOpenCloseOutlineProvider(expandedMenu).createRevealAnimator(expandedMenu, true);
+    revealAnim.setInterpolator(accelerateDecelerateInterpolator);
+
+    // Animator 3: expanded view fade out
+    Animator fadeOut = ObjectAnimator.ofFloat(expandedView, "alpha", 1, 0);
+    fadeOut.setInterpolator(accelerateDecelerateInterpolator);
+
+    // Play all animation together
+    AnimatorSet collapseAnimatorSet = new AnimatorSet();
+    collapseAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION);
+    collapseAnimatorSet.playTogether(revealAnim, fadeOut, xValueAnimator);
+    collapseAnimatorSet.addListener(
+        new AnimatorListenerAdapter() {
+          @Override
+          public void onAnimationEnd(Animator animation) {
+            collapseAnimation = null;
+            expanded = false;
+
+            if (textShowing) {
+              // Will do resize once the text is done.
+              return;
+            }
+
+            // If this collapse was to come before a hide, do it now.
+            if (collapseEndAction == CollapseEnd.HIDE) {
+              hide();
+            }
+            collapseEndAction = CollapseEnd.NOTHING;
+
+            // If collapse on the right side, the primary button move left a bit after drawer
+            // visibility becoming GONE. To avoid it, we create a new ViewHolder.
+            // It also set primary button clickable back to true, so no need to reset manually.
+            replaceViewHolder();
+
+            // Resume normal gravity after any resizing is done.
+            handler.postDelayed(
                 () -> {
-                  collapseAnimation = null;
-                  expanded = false;
-
-                  if (textShowing) {
-                    // Will do resize once the text is done.
-                    return;
+                  overrideGravity = null;
+                  if (!viewHolder.isMoving()) {
+                    viewHolder.undoGravityOverride();
                   }
-
-                  // Set drawer visibility to INVISIBLE instead of GONE to keep primary button fixed
-                  viewHolder.setDrawerVisibility(View.INVISIBLE);
-
-                  // Do X-move and Y-move together
-                  int deltaX =
-                      (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
-                  int startX = windowParams.x;
-                  int startY = windowParams.y;
-                  float k =
-                      (savedYPosition != -1 && shouldRecoverYPosition)
-                          ? (savedYPosition - startY) / (float) deltaX
-                          : 0;
-                  Path path = new Path();
-                  path.moveTo(windowParams.x, windowParams.y);
-                  path.lineTo(
-                      windowParams.x - deltaX,
-                      (savedYPosition != -1 && shouldRecoverYPosition)
-                          ? savedYPosition
-                          : windowParams.y);
-                  // The position is not useful after collapse
-                  savedYPosition = -1;
-
-                  ValueAnimator animator = ValueAnimator.ofFloat(startX, startX - deltaX);
-                  animator.setInterpolator(new LinearOutSlowInInterpolator());
-                  animator.addUpdateListener(
-                      (valueAnimator) -> {
-                        // Update windowParams and the root layout.
-                        // We can't do ViewPropertyAnimation since it clips children.
-                        float newX = (float) valueAnimator.getAnimatedValue();
-                        if (k != 0) {
-                          windowParams.y = startY + (int) (Math.abs(newX - (float) startX) * k);
-                        }
-                        windowParams.x = (int) newX;
-                        windowManager.updateViewLayout(viewHolder.getRoot(), windowParams);
-                      });
-                  animator.addListener(
-                      new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                          // If collapse on the right side, the primary button move left a bit after
-                          // drawer
-                          // visibility becoming GONE. To avoid it, we create a new ViewHolder.
-                          replaceViewHolder();
-                        }
-                      });
-                  animator.start();
-
-                  // If this collapse was to come before a hide, do it now.
-                  if (collapseEndAction == CollapseEnd.HIDE) {
-                    hide();
-                  }
-                  collapseEndAction = CollapseEnd.NOTHING;
-
-                  // Resume normal gravity after any resizing is done.
-                  handler.postDelayed(
-                      () -> {
-                        overrideGravity = null;
-                        if (!viewHolder.isMoving()) {
-                          viewHolder.undoGravityOverride();
-                        }
-                      },
-                      // Need to wait twice as long for resize and layout
-                      WINDOW_REDRAW_DELAY_MILLIS * 2);
-                });
+                },
+                // Need to wait twice as long for resize and layout
+                WINDOW_REDRAW_DELAY_MILLIS * 2);
+          }
+        });
+    collapseAnimatorSet.start();
   }
 
   /**
@@ -917,18 +916,45 @@
             });
   }
 
+  private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider(View view) {
+    int startRectX = isDrawingFromRight() ? view.getMeasuredWidth() : 0;
+    Rect startRect = new Rect(startRectX, 0, startRectX, 0);
+    Rect endRect = new Rect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
+
+    float bubbleRadius = context.getResources().getDimension(R.dimen.bubble_radius);
+    return new RoundedRectRevealOutlineProvider(bubbleRadius, bubbleRadius, startRect, endRect);
+  }
+
+  private ValueAnimator createBubbleMoveAnimator(int startX, int endX, int startY, float k) {
+    ValueAnimator xValueAnimator = ValueAnimator.ofFloat(startX, endX);
+    xValueAnimator.setInterpolator(new LinearOutSlowInInterpolator());
+    xValueAnimator.addUpdateListener(
+        (valueAnimator) -> {
+          // Update windowParams and the root layout.
+          // We can't do ViewPropertyAnimation since it clips children.
+          float newX = (float) valueAnimator.getAnimatedValue();
+          if (k != 0) {
+            windowParams.y = startY + (int) (Math.abs(newX - (float) startX) * k);
+          }
+          windowParams.x = (int) newX;
+          windowManager.updateViewLayout(viewHolder.getRoot(), windowParams);
+        });
+    return xValueAnimator;
+  }
+
   @VisibleForTesting
   class ViewHolder {
 
     public static final int CHILD_INDEX_AVATAR_AND_ICON = 0;
     public static final int CHILD_INDEX_TEXT = 1;
 
-    private final NewMoveHandler moveHandler;
+    private NewMoveHandler moveHandler;
     private final NewWindowRoot root;
     private final ViewAnimator primaryButton;
     private final ImageView primaryIcon;
     private final ImageView primaryAvatar;
     private final TextView primaryText;
+    private final View arrow;
 
     private final NewCheckableButton fullScreenButton;
     private final NewCheckableButton muteButton;
@@ -946,6 +972,7 @@
       primaryAvatar = contentView.findViewById(R.id.bubble_icon_avatar);
       primaryIcon = contentView.findViewById(R.id.bubble_icon_primary);
       primaryText = contentView.findViewById(R.id.bubble_text);
+      arrow = contentView.findViewById(R.id.bubble_triangle);
 
       fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen);
       muteButton = contentView.findViewById(R.id.bubble_button_mute);
@@ -985,8 +1012,10 @@
       muteButton.setClickable(clickable);
       audioRouteButton.setClickable(clickable);
       endCallButton.setClickable(clickable);
+      setPrimaryButtonClickable(clickable);
+    }
 
-      // For primaryButton
+    private void setPrimaryButtonClickable(boolean clickable) {
       moveHandler.setClickable(clickable);
     }
 
@@ -1024,6 +1053,10 @@
       return expandedView;
     }
 
+    public View getArrow() {
+      return arrow;
+    }
+
     public NewCheckableButton getFullScreenButton() {
       return fullScreenButton;
     }
diff --git a/java/com/android/newbubble/RoundedRectRevealOutlineProvider.java b/java/com/android/newbubble/RoundedRectRevealOutlineProvider.java
new file mode 100644
index 0000000..d204e0f
--- /dev/null
+++ b/java/com/android/newbubble/RoundedRectRevealOutlineProvider.java
@@ -0,0 +1,105 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+/**
+ * A {@link ViewOutlineProvider} that provides an outline that interpolates between two radii and
+ * two {@link Rect}s.
+ *
+ * <p>An example usage of this provider is an outline that starts out as a circle and ends as a
+ * rounded rectangle.
+ */
+public class RoundedRectRevealOutlineProvider extends ViewOutlineProvider {
+  private final float mStartRadius;
+  private final float mEndRadius;
+
+  private final Rect mStartRect;
+  private final Rect mEndRect;
+
+  private final Rect mOutline;
+  private float mOutlineRadius;
+
+  public RoundedRectRevealOutlineProvider(
+      float startRadius, float endRadius, Rect startRect, Rect endRect) {
+    mStartRadius = startRadius;
+    mEndRadius = endRadius;
+    mStartRect = startRect;
+    mEndRect = endRect;
+
+    mOutline = new Rect();
+  }
+
+  @Override
+  public void getOutline(View v, Outline outline) {
+    outline.setRoundRect(mOutline, mOutlineRadius);
+  }
+
+  /** Sets the progress, from 0 to 1, of the reveal animation. */
+  public void setProgress(float progress) {
+    mOutlineRadius = (1 - progress) * mStartRadius + progress * mEndRadius;
+
+    mOutline.left = (int) ((1 - progress) * mStartRect.left + progress * mEndRect.left);
+    mOutline.top = (int) ((1 - progress) * mStartRect.top + progress * mEndRect.top);
+    mOutline.right = (int) ((1 - progress) * mStartRect.right + progress * mEndRect.right);
+    mOutline.bottom = (int) ((1 - progress) * mStartRect.bottom + progress * mEndRect.bottom);
+  }
+
+  ValueAnimator createRevealAnimator(final View revealView, boolean isReversed) {
+    ValueAnimator valueAnimator =
+        isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f);
+
+    valueAnimator.addListener(
+        new AnimatorListenerAdapter() {
+          private boolean mWasCanceled = false;
+
+          @Override
+          public void onAnimationStart(Animator animation) {
+            revealView.setOutlineProvider(RoundedRectRevealOutlineProvider.this);
+            revealView.setClipToOutline(true);
+          }
+
+          @Override
+          public void onAnimationCancel(Animator animation) {
+            mWasCanceled = true;
+          }
+
+          @Override
+          public void onAnimationEnd(Animator animation) {
+            if (!mWasCanceled) {
+              revealView.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
+              revealView.setClipToOutline(false);
+            }
+          }
+        });
+
+    valueAnimator.addUpdateListener(
+        (currentValueAnimator) -> {
+          float progress = (Float) currentValueAnimator.getAnimatedValue();
+          setProgress(progress);
+          revealView.invalidateOutline();
+        });
+    return valueAnimator;
+  }
+}
diff --git a/java/com/android/newbubble/res/layout/new_bubble_base.xml b/java/com/android/newbubble/res/layout/new_bubble_base.xml
index 216dce0..f83b753 100644
--- a/java/com/android/newbubble/res/layout/new_bubble_base.xml
+++ b/java/com/android/newbubble/res/layout/new_bubble_base.xml
@@ -108,6 +108,7 @@
           android:rotation="45">
       </RelativeLayout>
       <RelativeLayout
+          android:id="@+id/bubble_expanded_menu"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_below="@id/bubble_triangle"
diff --git a/java/com/android/voicemail/AndroidManifest.xml b/java/com/android/voicemail/AndroidManifest.xml
index 817cf1b..d64fb25 100644
--- a/java/com/android/voicemail/AndroidManifest.xml
+++ b/java/com/android/voicemail/AndroidManifest.xml
@@ -18,7 +18,7 @@
 
   <uses-sdk
     android:minSdkVersion="23"
-    android:targetSdkVersion="26"/>
+    android:targetSdkVersion="27"/>
 
   <!-- Applications using this module should merge these permissions using android_manifest_merge -->