Merge changes I6806c5c8,I60afe7c6,I6d103569,I4cd6f76b,I6ddbebc9, ...

* changes:
  Hide bubble in (New)ReturnToCallController.tearDown().
  Grouping each listener's logic in an inner class in CallDetailsActivity.
  Allow VVM activation to be rerun if carrier sent STATUS SMS
  Check for null subscription info list in TelecomUtil#getSubscriptionInfo.
  Ignore "new" voicemails that are too old for notifications
  Display the automatically detected home country in assisted dialing settings.
  Add DATA_USAGE column to the annotated call log.
  Temporarily disable PhoneHistoryRecorder.
  Bubble v2 animation changes.
  Refactor Assisted Dialing Settings into standalone activity.
  Added PhoneLookupHistoryRecorder.
  Added PhoneLookupSelector.
diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java
index f962e17..4745736 100644
--- a/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java
+++ b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java
@@ -28,6 +28,7 @@
 import android.provider.CallLog.Calls;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
 import android.support.annotation.WorkerThread;
 import android.support.v4.os.UserManagerCompat;
 import android.telephony.PhoneNumberUtils;
@@ -35,7 +36,9 @@
 import com.android.dialer.app.R;
 import com.android.dialer.calllogutils.PhoneNumberDisplayUtil;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.database.Selection;
 import com.android.dialer.compat.android.provider.VoicemailCompat;
+import com.android.dialer.configprovider.ConfigProviderBindings;
 import com.android.dialer.location.GeoUtil;
 import com.android.dialer.phonenumbercache.ContactInfo;
 import com.android.dialer.phonenumbercache.ContactInfoHelper;
@@ -43,11 +46,16 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 /** Helper class operating on call log notifications. */
 @TargetApi(Build.VERSION_CODES.M)
 public class CallLogNotificationsQueryHelper {
 
+  @VisibleForTesting
+  static final String CONFIG_NEW_VOICEMAIL_NOTIFICATION_THRESHOLD_OFFSET =
+      "new_voicemail_notification_threshold";
+
   private final Context mContext;
   private final NewCallsQuery mNewCallsQuery;
   private final ContactInfoHelper mContactInfoHelper;
@@ -147,7 +155,12 @@
    */
   @Nullable
   public List<NewCall> getNewVoicemails() {
-    return mNewCallsQuery.query(Calls.VOICEMAIL_TYPE);
+    return mNewCallsQuery.query(
+        Calls.VOICEMAIL_TYPE,
+        System.currentTimeMillis()
+            - ConfigProviderBindings.get(mContext)
+                .getLong(
+                    CONFIG_NEW_VOICEMAIL_NOTIFICATION_THRESHOLD_OFFSET, TimeUnit.DAYS.toMillis(7)));
   }
 
   /**
@@ -220,10 +233,21 @@
   /** Allows determining the new calls for which a notification should be generated. */
   public interface NewCallsQuery {
 
+    long NO_THRESHOLD = Long.MAX_VALUE;
+
     /** Returns the new calls of a certain type for which a notification should be generated. */
     @Nullable
     List<NewCall> query(int type);
 
+    /**
+     * Returns the new calls of a certain type for which a notification should be generated.
+     *
+     * @param thresholdMillis New calls added before this timestamp will be considered old, or
+     *     {@link #NO_THRESHOLD} if threshold is not checked.
+     */
+    @Nullable
+    List<NewCall> query(int type, long thresholdMillis);
+
     /** Returns a {@link NewCall} pointed by the {@code callsUri} */
     @Nullable
     NewCall query(Uri callsUri);
@@ -317,6 +341,14 @@
     @Nullable
     @TargetApi(Build.VERSION_CODES.M)
     public List<NewCall> query(int type) {
+      return query(type, NO_THRESHOLD);
+    }
+
+    @Override
+    @Nullable
+    @TargetApi(Build.VERSION_CODES.M)
+    @SuppressWarnings("MissingPermission")
+    public List<NewCall> query(int type, long thresholdMillis) {
       if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) {
         LogUtil.w(
             "CallLogNotificationsQueryHelper.DefaultNewCallsQuery.query",
@@ -328,15 +360,27 @@
       // TYPE matches the query type.
       // IS_READ is not 1. A call might be backed up and restored, so it will be "new" to the
       //   call log, but the user has already read it on another device.
-      final String selection =
-          String.format("%s = 1 AND %s = ? AND %s IS NOT 1", Calls.NEW, Calls.TYPE, Calls.IS_READ);
-      final String[] selectionArgs = new String[] {Integer.toString(type)};
+      Selection.Builder selectionBuilder =
+          Selection.builder()
+              .and(Selection.column(Calls.NEW).is("= 1"))
+              .and(Selection.column(Calls.TYPE).is("=", type))
+              .and(Selection.column(Calls.IS_READ).is("IS NOT 1"));
+      if (thresholdMillis != NO_THRESHOLD) {
+        selectionBuilder =
+            selectionBuilder.and(
+                Selection.column(Calls.DATE)
+                    .is("IS NULL")
+                    .buildUpon()
+                    .or(Selection.column(Calls.DATE).is(">=", thresholdMillis))
+                    .build());
+      }
+      Selection selection = selectionBuilder.build();
       try (Cursor cursor =
           mContentResolver.query(
               Calls.CONTENT_URI_WITH_VOICEMAIL,
               (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) ? PROJECTION_O : PROJECTION,
-              selection,
-              selectionArgs,
+              selection.getSelection(),
+              selection.getSelectionArgs(),
               Calls.DEFAULT_SORT_ORDER)) {
         if (cursor == null) {
           return null;
diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
index fbd6f48..6410955 100644
--- a/java/com/android/dialer/app/settings/DialerSettingsActivity.java
+++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
@@ -35,7 +35,6 @@
 import com.android.dialer.about.AboutPhoneFragment;
 import com.android.dialer.app.R;
 import com.android.dialer.assisteddialing.ConcreteCreator;
-import com.android.dialer.assisteddialing.ui.AssistedDialingSettingFragment;
 import com.android.dialer.blocking.FilteredNumberCompat;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.telephony.TelephonyManagerCompat;
@@ -170,7 +169,8 @@
       Header assistedDialingSettingsHeader = new Header();
       assistedDialingSettingsHeader.titleRes =
           com.android.dialer.assisteddialing.ui.R.string.assisted_dialing_setting_title;
-      assistedDialingSettingsHeader.fragment = AssistedDialingSettingFragment.class.getName();
+      assistedDialingSettingsHeader.intent =
+          new Intent("com.android.dialer.app.settings.SHOW_ASSISTED_DIALING_SETTINGS");
       target.add(assistedDialingSettingsHeader);
     }
 
diff --git a/java/com/android/dialer/assisteddialing/ui/AndroidManifest.xml b/java/com/android/dialer/assisteddialing/ui/AndroidManifest.xml
index 7248747..5266b13 100644
--- a/java/com/android/dialer/assisteddialing/ui/AndroidManifest.xml
+++ b/java/com/android/dialer/assisteddialing/ui/AndroidManifest.xml
@@ -19,4 +19,17 @@
       android:minSdkVersion="23"
       android:targetSdkVersion="24"/>
 
+  <application>
+    <activity
+        android:label="@string/assisted_dialing_setting_title"
+        android:name=".AssistedDialingSettingActivity"
+        android:parentActivityName="com.android.dialer.app.settings.DialerSettingsActivity"
+        android:theme="@style/SettingsStyle">
+      <intent-filter>
+        <action android:name="com.android.dialer.app.settings.SHOW_ASSISTED_DIALING_SETTINGS"/>
+        <category android:name="android.intent.category.DEFAULT"/>
+      </intent-filter>
+    </activity>
+  </application>
+
 </manifest>
\ No newline at end of file
diff --git a/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingActivity.java b/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingActivity.java
new file mode 100644
index 0000000..ca36745
--- /dev/null
+++ b/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingActivity.java
@@ -0,0 +1,44 @@
+/*
+ * 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.dialer.assisteddialing.ui;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+
+/** The Settings Activity for Assisted Dialing. */
+public class AssistedDialingSettingActivity extends AppCompatActivity {
+
+  @Override
+  protected void onCreate(Bundle bundle) {
+    super.onCreate(bundle);
+
+    getFragmentManager()
+        .beginTransaction()
+        .replace(android.R.id.content, new AssistedDialingSettingFragment())
+        .commit();
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    if (item.getItemId() == android.R.id.home) {
+      onBackPressed();
+      return true;
+    }
+    return super.onOptionsItemSelected(item);
+  }
+}
diff --git a/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingFragment.java b/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingFragment.java
index 0341894..d4fb3f6 100644
--- a/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingFragment.java
+++ b/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingFragment.java
@@ -22,15 +22,18 @@
 import android.preference.Preference;
 import android.preference.PreferenceFragment;
 import android.preference.SwitchPreference;
-import android.text.TextUtils;
+import android.telephony.TelephonyManager;
+import com.android.dialer.assisteddialing.AssistedDialingMediator;
 import com.android.dialer.assisteddialing.ConcreteCreator;
 import com.android.dialer.assisteddialing.CountryCodeProvider;
+import com.android.dialer.common.LogUtil;
 import com.android.dialer.configprovider.ConfigProviderBindings;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
 import com.google.auto.value.AutoValue;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 
 /** The setting for Assisted Dialing */
 @TargetApi(VERSION_CODES.N)
@@ -38,6 +41,7 @@
 public class AssistedDialingSettingFragment extends PreferenceFragment {
 
   private CountryCodeProvider countryCodeProvider;
+  private AssistedDialingMediator assistedDialingMediator;
 
   @AutoValue
   abstract static class DisplayNameAndCountryCodeTuple {
@@ -59,6 +63,10 @@
   public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
 
+    assistedDialingMediator =
+        ConcreteCreator.createNewAssistedDialingMediator(
+            getContext().getSystemService(TelephonyManager.class), getContext());
+
     countryCodeProvider =
         ConcreteCreator.getCountryCodeProvider(ConfigProviderBindings.get(getContext()));
 
@@ -73,14 +81,39 @@
             findPreference(getContext().getString(R.string.assisted_dialing_setting_cc_key));
 
     updateCountryChoices(countryChooserPref);
+    updateCountryChooserSummary(countryChooserPref);
 
-    if (!TextUtils.isEmpty(countryChooserPref.getEntry())) {
-      countryChooserPref.setSummary(countryChooserPref.getEntry());
-    }
     countryChooserPref.setOnPreferenceChangeListener(this::updateListSummary);
     switchPref.setOnPreferenceChangeListener(this::logIfUserDisabledFeature);
   }
 
+  private void updateCountryChooserSummary(ListPreference countryChooserPref) {
+    String defaultSummaryText = countryChooserPref.getEntries()[0].toString();
+
+    if (countryChooserPref.getEntry().equals(defaultSummaryText)) {
+      Optional<String> userHomeCountryCode = assistedDialingMediator.userHomeCountryCode();
+      if (userHomeCountryCode.isPresent()) {
+        CharSequence[] entries = countryChooserPref.getEntries();
+        try {
+          CharSequence regionalDisplayName =
+              entries[countryChooserPref.findIndexOfValue(userHomeCountryCode.get())];
+          countryChooserPref.setSummary(
+              getContext()
+                  .getString(
+                      R.string.assisted_dialing_setting_cc_default_summary, regionalDisplayName));
+        } catch (ArrayIndexOutOfBoundsException e) {
+          // This might happen if there is a mismatch between the automatically
+          // detected home country, and the countries currently eligible to select in the settings.
+          LogUtil.i(
+              "AssistedDialingSettingFragment.onCreate",
+              "Failed to find human readable mapping for country code, using default.");
+        }
+      }
+    } else {
+      countryChooserPref.setSummary(countryChooserPref.getEntry());
+    }
+  }
+
   /**
    * Filters the default entries in the country chooser by only showing those countries in which the
    * feature in enabled.
diff --git a/java/com/android/dialer/assisteddialing/ui/res/values/strings.xml b/java/com/android/dialer/assisteddialing/ui/res/values/strings.xml
index 3a81780..35aa2f1 100644
--- a/java/com/android/dialer/assisteddialing/ui/res/values/strings.xml
+++ b/java/com/android/dialer/assisteddialing/ui/res/values/strings.xml
@@ -23,7 +23,10 @@
   <string name="assisted_dialing_setting_summary">Predict and add a country code when you call while traveling abroad</string>
 
   <!-- Indicates the default state for the home country selector-->
-  <string name="assisted_dialing_setting_cc_default_summary">Automatically detected</string>
+  <string name="assisted_dialing_setting_cc_default_summary">Automatically detected • <xliff:g example="United Kingdom (+44)" id="ad_country_code_info">%1$s</xliff:g></string>
+
+  <!-- Indicates the default failure state for the home country selector-->
+  <string name="assisted_dialing_setting_cc_default_summary_fallback">Automatically detected</string>
 
   <!-- Category title for the country code picker in assisted dialing [CHAR LIMIT=40]-->
   <string name="assisted_dialing_setting_cc_category_title">Home country</string>
@@ -36,7 +39,7 @@
   <!-- Excluding ['Antarctica', 'Bouvet Island', 'French Southern Territories (the)', 'Heard Island and McDonald Islands', 'Pitcairn', 'South Georgia and the South Sandwich Islands', 'United States Minor Outlying Islands (the)'] -->
   <!-- Options for the country codes used in assisted dialing. DO NOT TRANSLATE NUMBERS. [CHAR LIMIT=40] -->
   <string-array name="assisted_dialing_cc_entries">
-    <item>@string/assisted_dialing_setting_cc_default_summary</item>
+    <item>@string/assisted_dialing_setting_cc_default_summary_fallback</item>
     <item>Afghanistan <xliff:g>(+93)</xliff:g></item>
     <item>Åland Islands <xliff:g>(+358)</xliff:g></item>
     <item>Albania <xliff:g>(+355)</xliff:g></item>
diff --git a/java/com/android/dialer/assisteddialing/ui/res/xml/assisted_dialing_setting.xml b/java/com/android/dialer/assisteddialing/ui/res/xml/assisted_dialing_setting.xml
index c1706b2..9fb61a1 100644
--- a/java/com/android/dialer/assisteddialing/ui/res/xml/assisted_dialing_setting.xml
+++ b/java/com/android/dialer/assisteddialing/ui/res/xml/assisted_dialing_setting.xml
@@ -31,7 +31,7 @@
         android:entries="@array/assisted_dialing_cc_entries"
         android:entryValues="@array/assisted_dialing_cc_values"
         android:key="@string/assisted_dialing_setting_cc_key"
-        android:summary="@string/assisted_dialing_setting_cc_default_summary"
+        android:summary="@string/assisted_dialing_setting_cc_default_summary_fallback"
         android:title="@string/assisted_dialing_setting_cc_title"/>
   </PreferenceCategory>
 
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java
index 06b6a10..b51d833 100644
--- a/java/com/android/dialer/calldetails/CallDetailsActivity.java
+++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java
@@ -35,7 +35,6 @@
 import android.support.v7.widget.Toolbar;
 import android.widget.Toast;
 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
-import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener;
 import com.android.dialer.callintent.CallInitiationType;
 import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.common.Assert;
@@ -46,7 +45,7 @@
 import com.android.dialer.duo.Duo;
 import com.android.dialer.duo.DuoComponent;
 import com.android.dialer.enrichedcall.EnrichedCallComponent;
-import com.android.dialer.enrichedcall.EnrichedCallManager.HistoricalDataChangedListener;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
 import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
@@ -55,17 +54,14 @@
 import com.android.dialer.postcall.PostCall;
 import com.android.dialer.precall.PreCall;
 import com.android.dialer.protos.ProtoParsers;
+import com.google.common.base.Preconditions;
 import java.lang.ref.WeakReference;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
 /** Displays the details of a specific call log entry. */
-public class CallDetailsActivity extends AppCompatActivity
-    implements CallDetailsHeaderViewHolder.CallbackActionListener,
-        CallDetailsFooterViewHolder.ReportCallIdListener,
-        DeleteCallDetailsListener,
-        HistoricalDataChangedListener {
+public class CallDetailsActivity extends AppCompatActivity {
 
   public static final String EXTRA_PHONE_NUMBER = "phone_number";
   public static final String EXTRA_HAS_ENRICHED_CALL_DATA = "has_enriched_call_data";
@@ -73,7 +69,16 @@
   public static final String EXTRA_CONTACT = "contact";
   public static final String EXTRA_CAN_REPORT_CALLER_ID = "can_report_caller_id";
   private static final String EXTRA_CAN_SUPPORT_ASSISTED_DIALING = "can_support_assisted_dialing";
-  private static final String TASK_DELETE = "task_delete";
+
+  private final CallDetailsHeaderViewHolder.CallbackActionListener callbackActionListener =
+      new CallbackActionListener(this);
+  private final CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener =
+      new DeleteCallDetailsListener(this);
+  private final CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener =
+      new ReportCallIdListener(this);
+  private final EnrichedCallManager.HistoricalDataChangedListener
+      enrichedCallHistoricalDataChangedListener =
+          new EnrichedCallHistoricalDataChangedListener(this);
 
   private CallDetailsEntries entries;
   private DialerContact contact;
@@ -130,7 +135,7 @@
 
     EnrichedCallComponent.get(this)
         .getEnrichedCallManager()
-        .registerHistoricalDataChangedListener(this);
+        .registerHistoricalDataChangedListener(enrichedCallHistoricalDataChangedListener);
     EnrichedCallComponent.get(this)
         .getEnrichedCallManager()
         .requestAllHistoricalData(contact.getNumber(), entries);
@@ -142,7 +147,7 @@
 
     EnrichedCallComponent.get(this)
         .getEnrichedCallManager()
-        .unregisterHistoricalDataChangedListener(this);
+        .unregisterHistoricalDataChangedListener(enrichedCallHistoricalDataChangedListener);
   }
 
   @Override
@@ -161,9 +166,9 @@
             this /* context */,
             contact,
             entries.getEntriesList(),
-            this /* callbackListener */,
-            this /* reportCallIdListener */,
-            this /* callDetailDeletionListener */);
+            callbackActionListener,
+            reportCallIdListener,
+            deleteCallDetailsListener);
 
     RecyclerView recyclerView = findViewById(R.id.recycler_view);
     recyclerView.setLayoutManager(new LinearLayoutManager(this));
@@ -177,113 +182,6 @@
     super.onBackPressed();
   }
 
-  @Override
-  public void reportCallId(String number) {
-    ReportDialogFragment.newInstance(number).show(getFragmentManager(), null);
-  }
-
-  @Override
-  public boolean canReportCallerId(String number) {
-    return getIntent().getExtras().getBoolean(EXTRA_CAN_REPORT_CALLER_ID, false);
-  }
-
-  @Override
-  public void onHistoricalDataChanged() {
-    Map<CallDetailsEntry, List<HistoryResult>> mappedResults =
-        getAllHistoricalData(contact.getNumber(), entries);
-
-    adapter.updateCallDetailsEntries(
-        generateAndMapNewCallDetailsEntriesHistoryResults(
-                contact.getNumber(), entries, mappedResults)
-            .getEntriesList());
-  }
-
-  @Override
-  public void placeImsVideoCall(String phoneNumber) {
-    Logger.get(this).logImpression(DialerImpression.Type.CALL_DETAILS_IMS_VIDEO_CALL_BACK);
-    PreCall.start(
-        this,
-        new CallIntentBuilder(phoneNumber, CallInitiationType.Type.CALL_DETAILS)
-            .setIsVideoCall(true));
-  }
-
-  @Override
-  public void placeDuoVideoCall(String phoneNumber) {
-    Logger.get(this).logImpression(DialerImpression.Type.CALL_DETAILS_LIGHTBRINGER_CALL_BACK);
-    Duo duo = DuoComponent.get(this).getDuo();
-    if (!duo.isReachable(this, phoneNumber)) {
-      placeImsVideoCall(phoneNumber);
-      return;
-    }
-
-    try {
-      startActivityForResult(duo.getIntent(this, phoneNumber), ActivityRequestCodes.DIALTACTS_DUO);
-    } catch (ActivityNotFoundException e) {
-      Toast.makeText(this, R.string.activity_not_available, Toast.LENGTH_SHORT).show();
-    }
-  }
-
-  @Override
-  public void placeVoiceCall(String phoneNumber, String postDialDigits) {
-    Logger.get(this).logImpression(DialerImpression.Type.CALL_DETAILS_VOICE_CALL_BACK);
-
-    boolean canSupportedAssistedDialing =
-        getIntent().getExtras().getBoolean(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, false);
-    CallIntentBuilder callIntentBuilder =
-        new CallIntentBuilder(phoneNumber + postDialDigits, CallInitiationType.Type.CALL_DETAILS);
-    if (canSupportedAssistedDialing) {
-      callIntentBuilder.setAllowAssistedDial(true);
-    }
-
-    PreCall.start(this, callIntentBuilder);
-  }
-
-  @Override
-  public void delete() {
-    AsyncTaskExecutors.createAsyncTaskExecutor()
-        .submit(TASK_DELETE, new DeleteCallsTask(this, contact, entries));
-  }
-
-  @NonNull
-  private Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData(
-      @Nullable String number, @NonNull CallDetailsEntries entries) {
-    if (number == null) {
-      return Collections.emptyMap();
-    }
-
-    Map<CallDetailsEntry, List<HistoryResult>> historicalData =
-        EnrichedCallComponent.get(this)
-            .getEnrichedCallManager()
-            .getAllHistoricalData(number, entries);
-    if (historicalData == null) {
-      return Collections.emptyMap();
-    }
-    return historicalData;
-  }
-
-  private static CallDetailsEntries generateAndMapNewCallDetailsEntriesHistoryResults(
-      @Nullable String number,
-      @NonNull CallDetailsEntries callDetailsEntries,
-      @NonNull Map<CallDetailsEntry, List<HistoryResult>> mappedResults) {
-    if (number == null) {
-      return callDetailsEntries;
-    }
-    CallDetailsEntries.Builder mutableCallDetailsEntries = CallDetailsEntries.newBuilder();
-    for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) {
-      CallDetailsEntry.Builder newEntry = CallDetailsEntry.newBuilder().mergeFrom(entry);
-      List<HistoryResult> results = mappedResults.get(entry);
-      if (results != null) {
-        newEntry.addAllHistoryResults(mappedResults.get(entry));
-        LogUtil.v(
-            "CallLogAdapter.generateAndMapNewCallDetailsEntriesHistoryResults",
-            "mapped %d results",
-            newEntry.getHistoryResultsList().size());
-      }
-      mutableCallDetailsEntries.addEntries(newEntry.build());
-    }
-    return mutableCallDetailsEntries.build();
-  }
-
   /** Delete specified calls from the call log. */
   private static class DeleteCallsTask extends AsyncTask<Void, Void, Void> {
     // Use a weak reference to hold the Activity so that there is no memory leak.
@@ -349,4 +247,176 @@
       activity.finish();
     }
   }
+
+  private static final class CallbackActionListener
+      implements CallDetailsHeaderViewHolder.CallbackActionListener {
+    private final WeakReference<Activity> activityWeakReference;
+
+    CallbackActionListener(Activity activity) {
+      this.activityWeakReference = new WeakReference<>(activity);
+    }
+
+    @Override
+    public void placeImsVideoCall(String phoneNumber) {
+      Logger.get(getActivity())
+          .logImpression(DialerImpression.Type.CALL_DETAILS_IMS_VIDEO_CALL_BACK);
+      PreCall.start(
+          getActivity(),
+          new CallIntentBuilder(phoneNumber, CallInitiationType.Type.CALL_DETAILS)
+              .setIsVideoCall(true));
+    }
+
+    @Override
+    public void placeDuoVideoCall(String phoneNumber) {
+      Logger.get(getActivity())
+          .logImpression(DialerImpression.Type.CALL_DETAILS_LIGHTBRINGER_CALL_BACK);
+      Duo duo = DuoComponent.get(getActivity()).getDuo();
+      if (!duo.isReachable(getActivity(), phoneNumber)) {
+        placeImsVideoCall(phoneNumber);
+        return;
+      }
+
+      try {
+        getActivity()
+            .startActivityForResult(
+                duo.getIntent(getActivity(), phoneNumber), ActivityRequestCodes.DIALTACTS_DUO);
+      } catch (ActivityNotFoundException e) {
+        Toast.makeText(getActivity(), R.string.activity_not_available, Toast.LENGTH_SHORT).show();
+      }
+    }
+
+    @Override
+    public void placeVoiceCall(String phoneNumber, String postDialDigits) {
+      Logger.get(getActivity()).logImpression(DialerImpression.Type.CALL_DETAILS_VOICE_CALL_BACK);
+
+      boolean canSupportedAssistedDialing =
+          getActivity()
+              .getIntent()
+              .getExtras()
+              .getBoolean(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, false);
+      CallIntentBuilder callIntentBuilder =
+          new CallIntentBuilder(phoneNumber + postDialDigits, CallInitiationType.Type.CALL_DETAILS);
+      if (canSupportedAssistedDialing) {
+        callIntentBuilder.setAllowAssistedDial(true);
+      }
+
+      PreCall.start(getActivity(), callIntentBuilder);
+    }
+
+    private Activity getActivity() {
+      return Preconditions.checkNotNull(activityWeakReference.get());
+    }
+  }
+
+  private static final class DeleteCallDetailsListener
+      implements CallDetailsFooterViewHolder.DeleteCallDetailsListener {
+    private static final String ASYNC_TASK_ID = "task_delete";
+
+    private final WeakReference<CallDetailsActivity> activityWeakReference;
+
+    DeleteCallDetailsListener(CallDetailsActivity activity) {
+      this.activityWeakReference = new WeakReference<>(activity);
+    }
+
+    @Override
+    public void delete() {
+      AsyncTaskExecutors.createAsyncTaskExecutor()
+          .submit(
+              ASYNC_TASK_ID,
+              new DeleteCallsTask(getActivity(), getActivity().contact, getActivity().entries));
+    }
+
+    private CallDetailsActivity getActivity() {
+      return Preconditions.checkNotNull(activityWeakReference.get());
+    }
+  }
+
+  private static final class ReportCallIdListener
+      implements CallDetailsFooterViewHolder.ReportCallIdListener {
+    private final WeakReference<Activity> activityWeakReference;
+
+    ReportCallIdListener(Activity activity) {
+      this.activityWeakReference = new WeakReference<>(activity);
+    }
+
+    @Override
+    public void reportCallId(String number) {
+      ReportDialogFragment.newInstance(number)
+          .show(getActivity().getFragmentManager(), null /* tag */);
+    }
+
+    @Override
+    public boolean canReportCallerId(String number) {
+      return getActivity().getIntent().getExtras().getBoolean(EXTRA_CAN_REPORT_CALLER_ID, false);
+    }
+
+    private Activity getActivity() {
+      return Preconditions.checkNotNull(activityWeakReference.get());
+    }
+  }
+
+  private static final class EnrichedCallHistoricalDataChangedListener
+      implements EnrichedCallManager.HistoricalDataChangedListener {
+    private final WeakReference<CallDetailsActivity> activityWeakReference;
+
+    EnrichedCallHistoricalDataChangedListener(CallDetailsActivity activity) {
+      this.activityWeakReference = new WeakReference<>(activity);
+    }
+
+    @Override
+    public void onHistoricalDataChanged() {
+      CallDetailsActivity activity = getActivity();
+      Map<CallDetailsEntry, List<HistoryResult>> mappedResults =
+          getAllHistoricalData(activity.contact.getNumber(), activity.entries);
+
+      activity.adapter.updateCallDetailsEntries(
+          generateAndMapNewCallDetailsEntriesHistoryResults(
+                  activity.contact.getNumber(), activity.entries, mappedResults)
+              .getEntriesList());
+    }
+
+    private CallDetailsActivity getActivity() {
+      return Preconditions.checkNotNull(activityWeakReference.get());
+    }
+
+    @NonNull
+    private Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData(
+        @Nullable String number, @NonNull CallDetailsEntries entries) {
+      if (number == null) {
+        return Collections.emptyMap();
+      }
+
+      Map<CallDetailsEntry, List<HistoryResult>> historicalData =
+          EnrichedCallComponent.get(getActivity())
+              .getEnrichedCallManager()
+              .getAllHistoricalData(number, entries);
+      if (historicalData == null) {
+        return Collections.emptyMap();
+      }
+      return historicalData;
+    }
+
+    private static CallDetailsEntries generateAndMapNewCallDetailsEntriesHistoryResults(
+        @Nullable String number,
+        @NonNull CallDetailsEntries callDetailsEntries,
+        @NonNull Map<CallDetailsEntry, List<HistoryResult>> mappedResults) {
+      if (number == null) {
+        return callDetailsEntries;
+      }
+      CallDetailsEntries.Builder mutableCallDetailsEntries = CallDetailsEntries.newBuilder();
+      for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) {
+        CallDetailsEntry.Builder newEntry = CallDetailsEntry.newBuilder().mergeFrom(entry);
+        List<HistoryResult> results = mappedResults.get(entry);
+        if (results != null) {
+          newEntry.addAllHistoryResults(mappedResults.get(entry));
+          LogUtil.v(
+              "CallDetailsActivity.generateAndMapNewCallDetailsEntriesHistoryResults",
+              "mapped %d results",
+              newEntry.getHistoryResultsList().size());
+        }
+        mutableCallDetailsEntries.addEntries(newEntry.build());
+      }
+      return mutableCallDetailsEntries.build();
+    }
+  }
 }
diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
index 0d8e8ce..8c6d586 100644
--- a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
+++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
@@ -45,6 +45,7 @@
           + (AnnotatedCallLog.PHOTO_ID + " integer, ")
           + (AnnotatedCallLog.LOOKUP_URI + " text, ")
           + (AnnotatedCallLog.DURATION + " integer, ")
+          + (AnnotatedCallLog.DATA_USAGE + " integer, ")
           + (AnnotatedCallLog.NUMBER_TYPE_LABEL + " text, ")
           + (AnnotatedCallLog.IS_READ + " integer, ")
           + (AnnotatedCallLog.NEW + " integer, ")
diff --git a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
index c9b463e..9efe214 100644
--- a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
+++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
@@ -225,6 +225,13 @@
     public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/annotated_call_log";
 
     /**
+     * See {@link android.provider.CallLog.Calls#DATA_USAGE}.
+     *
+     * <p>Type: INTEGER (long)
+     */
+    public static final String DATA_USAGE = "data_usage";
+
+    /**
      * See {@link android.provider.CallLog.Calls#DURATION}.
      *
      * <p>TYPE: INTEGER (long)
diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java
index 7a7f207..010cb85 100644
--- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java
@@ -31,6 +31,7 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.phonelookup.PhoneLookup;
 import com.android.dialer.phonelookup.PhoneLookupInfo;
+import com.android.dialer.phonelookup.PhoneLookupSelector;
 import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory;
 import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
 import com.google.common.collect.ImmutableMap;
@@ -376,11 +377,7 @@
     }
   }
 
-  // TODO(zachh): Extract this logic into a proper selection class or package.
   private static String selectName(PhoneLookupInfo phoneLookupInfo) {
-    if (phoneLookupInfo.getCp2Info().getCp2ContactInfoCount() > 0) {
-      return phoneLookupInfo.getCp2Info().getCp2ContactInfo(0).getName();
-    }
-    return "";
+    return PhoneLookupSelector.selectName(phoneLookupInfo);
   }
 }
diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
index 5ca1607..ef40c30 100644
--- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
@@ -220,6 +220,7 @@
                   Calls.CACHED_NUMBER_TYPE,
                   Calls.CACHED_NUMBER_LABEL,
                   Calls.DURATION,
+                  Calls.DATA_USAGE,
                   Calls.TRANSCRIPTION,
                   Calls.VOICEMAIL_URI,
                   Calls.IS_READ,
@@ -259,6 +260,7 @@
         int cachedNumberTypeColumn = cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_TYPE);
         int cachedNumberLabelColumn = cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_LABEL);
         int durationsColumn = cursor.getColumnIndexOrThrow(Calls.DURATION);
+        int dataUsageColumn = cursor.getColumnIndexOrThrow(Calls.DATA_USAGE);
         int transcriptionColumn = cursor.getColumnIndexOrThrow(Calls.TRANSCRIPTION);
         int voicemailUriColumn = cursor.getColumnIndexOrThrow(Calls.VOICEMAIL_URI);
         int isReadColumn = cursor.getColumnIndexOrThrow(Calls.IS_READ);
@@ -286,6 +288,7 @@
           int cachedNumberType = cursor.getInt(cachedNumberTypeColumn);
           String cachedNumberLabel = cursor.getString(cachedNumberLabelColumn);
           int duration = cursor.getInt(durationsColumn);
+          int dataUsage = cursor.getInt(dataUsageColumn);
           String transcription = cursor.getString(transcriptionColumn);
           String voicemailUri = cursor.getString(voicemailUriColumn);
           int isRead = cursor.getInt(isReadColumn);
@@ -334,6 +337,7 @@
               appContext, contentValues, phoneAccountComponentName, phoneAccountId);
           contentValues.put(AnnotatedCallLog.FEATURES, features);
           contentValues.put(AnnotatedCallLog.DURATION, duration);
+          contentValues.put(AnnotatedCallLog.DATA_USAGE, dataUsage);
           contentValues.put(AnnotatedCallLog.TRANSCRIPTION, transcription);
           contentValues.put(AnnotatedCallLog.VOICEMAIL_URI, voicemailUri);
 
diff --git a/java/com/android/dialer/phonelookup/PhoneLookupSelector.java b/java/com/android/dialer/phonelookup/PhoneLookupSelector.java
new file mode 100644
index 0000000..a746ea4
--- /dev/null
+++ b/java/com/android/dialer/phonelookup/PhoneLookupSelector.java
@@ -0,0 +1,47 @@
+/*
+ * 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.dialer.phonelookup;
+
+import android.support.annotation.NonNull;
+
+/**
+ * Prioritizes information from a {@link PhoneLookupInfo}.
+ *
+ * <p>For example, a single {@link PhoneLookupInfo} may contain different name information from many
+ * different {@link PhoneLookup} sources. This class defines the rules for deciding which name
+ * should be selected for display to the user, by prioritizing the data from some {@link PhoneLookup
+ * PhoneLookups} over others.
+ *
+ * <p>Note that the logic in this class may be highly coupled with the logic in {@code
+ * CompositePhoneLookup}, because {@code CompositePhoneLookup} may also include prioritization logic
+ * for short-circuiting low-priority {@link PhoneLookup PhoneLookups}.
+ */
+public final class PhoneLookupSelector {
+
+  /**
+   * Select the name associated with this number. Examples of this are a local contact's name or a
+   * business name received from caller ID.
+   */
+  @NonNull
+  public static String selectName(PhoneLookupInfo phoneLookupInfo) {
+    if (phoneLookupInfo.getCp2Info().getCp2ContactInfoCount() > 0) {
+      // Arbitrarily select the first contact's name. In the future, it may make sense to join the
+      // names such as "Mom, Dad" in the case that multiple contacts share the same number.
+      return phoneLookupInfo.getCp2Info().getCp2ContactInfo(0).getName();
+    }
+    return "";
+  }
+}
diff --git a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java
index 520c46f..f432e27 100644
--- a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java
@@ -53,6 +53,8 @@
    */
   @Override
   public ListenableFuture<PhoneLookupInfo> lookup(@NonNull Call call) {
+    // TODO(zachh): Add short-circuiting logic so that this call is not blocked on low-priority
+    // lookups finishing when a higher-priority one has already finished.
     List<ListenableFuture<PhoneLookupInfo>> futures = new ArrayList<>();
     for (PhoneLookup phoneLookup : phoneLookups) {
       futures.add(phoneLookup.lookup(call));
diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java
index 307f998..cfb8fb7 100644
--- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java
@@ -82,7 +82,8 @@
 
   @Override
   public ListenableFuture<PhoneLookupInfo> lookup(@NonNull Call call) {
-    throw new UnsupportedOperationException();
+    // TODO(zachh): Implementation.
+    return MoreExecutors.newDirectExecutorService().submit(PhoneLookupInfo::getDefaultInstance);
   }
 
   @Override
diff --git a/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java b/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java
index 27041e7..e85654e 100644
--- a/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java
+++ b/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java
@@ -201,6 +201,12 @@
     return rows;
   }
 
+  /**
+   * Note: If the normalized number is included as part of the URI (for example,
+   * "content://com.android.dialer.phonelookuphistory/PhoneLookupHistory/+123") then the update
+   * operation will actually be a "replace" operation, inserting a new row if one does not already
+   * exist.
+   */
   @Override
   public int update(
       @NonNull Uri uri,
@@ -214,7 +220,13 @@
     int match = uriMatcher.match(uri);
     switch (match) {
       case PHONE_LOOKUP_HISTORY_TABLE_CODE:
-        break;
+        int rows = database.update(PhoneLookupHistory.TABLE, values, selection, selectionArgs);
+        if (rows == 0) {
+          LogUtil.w("PhoneLookupHistoryContentProvider.update", "no rows updated");
+          return rows;
+        }
+        notifyChange(uri);
+        return rows;
       case PHONE_LOOKUP_HISTORY_TABLE_ID_CODE:
         Assert.checkArgument(
             !values.containsKey(PhoneLookupHistory.NORMALIZED_NUMBER),
@@ -222,19 +234,15 @@
         Assert.checkArgument(selection == null, "Do not specify selection when updating by ID");
         Assert.checkArgument(
             selectionArgs == null, "Do not specify selection args when updating by ID");
-        selection = PhoneLookupHistory.NORMALIZED_NUMBER + "= ?";
-        selectionArgs = new String[] {uri.getLastPathSegment()};
-        break;
+
+        String normalizedNumber = uri.getLastPathSegment();
+        values.put(PhoneLookupHistory.NORMALIZED_NUMBER, normalizedNumber);
+        database.replace(PhoneLookupHistory.TABLE, null, values);
+        notifyChange(uri);
+        return 1;
       default:
         throw new IllegalArgumentException("Unknown uri: " + uri);
     }
-    int rows = database.update(PhoneLookupHistory.TABLE, values, selection, selectionArgs);
-    if (rows == 0) {
-      LogUtil.w("PhoneLookupHistoryContentProvider.update", "no rows updated");
-      return rows;
-    }
-    notifyChange(uri);
-    return rows;
   }
 
   private void notifyChange(Uri uri) {
diff --git a/java/com/android/dialer/telecom/TelecomUtil.java b/java/com/android/dialer/telecom/TelecomUtil.java
index c79d901..6f424de 100644
--- a/java/com/android/dialer/telecom/TelecomUtil.java
+++ b/java/com/android/dialer/telecom/TelecomUtil.java
@@ -158,7 +158,11 @@
       return Optional.absent();
     }
     SubscriptionManager subscriptionManager = context.getSystemService(SubscriptionManager.class);
-    for (SubscriptionInfo info : subscriptionManager.getActiveSubscriptionInfoList()) {
+    List<SubscriptionInfo> subscriptionInfos = subscriptionManager.getActiveSubscriptionInfoList();
+    if (subscriptionInfos == null) {
+      return Optional.absent();
+    }
+    for (SubscriptionInfo info : subscriptionInfos) {
       if (phoneAccountHandle.getId().startsWith(info.getIccId())) {
         return Optional.of(info);
       }
diff --git a/java/com/android/incallui/NewReturnToCallController.java b/java/com/android/incallui/NewReturnToCallController.java
index 399b185..abff000 100644
--- a/java/com/android/incallui/NewReturnToCallController.java
+++ b/java/com/android/incallui/NewReturnToCallController.java
@@ -29,8 +29,6 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.configprovider.ConfigProviderBindings;
 import com.android.dialer.lettertile.LetterTileDrawable;
-import com.android.dialer.logging.DialerImpression;
-import com.android.dialer.logging.Logger;
 import com.android.dialer.telecom.TelecomUtil;
 import com.android.incallui.ContactInfoCache.ContactCacheEntry;
 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
@@ -43,8 +41,6 @@
 import com.android.incallui.speakerbuttonlogic.SpeakerButtonInfo;
 import com.android.incallui.speakerbuttonlogic.SpeakerButtonInfo.IconSize;
 import com.android.newbubble.NewBubble;
-import com.android.newbubble.NewBubble.BubbleExpansionStateListener;
-import com.android.newbubble.NewBubble.ExpansionState;
 import com.android.newbubble.NewBubbleInfo;
 import com.android.newbubble.NewBubbleInfo.Action;
 import java.lang.ref.WeakReference;
@@ -102,6 +98,7 @@
   }
 
   public void tearDown() {
+    hide();
     InCallPresenter.getInstance().removeInCallUiListener(this);
     CallList.getInstance().removeListener(this);
     AudioModeProvider.getInstance().removeListener(this);
@@ -150,45 +147,6 @@
       return null;
     }
     NewBubble returnToCallBubble = NewBubble.createBubble(context, generateBubbleInfo());
-    returnToCallBubble.setBubbleExpansionStateListener(
-        new BubbleExpansionStateListener() {
-          @Override
-          public void onBubbleExpansionStateChanged(
-              @ExpansionState int expansionState, boolean isUserAction) {
-            if (!isUserAction) {
-              return;
-            }
-
-            DialerCall call = CallList.getInstance().getActiveOrBackgroundCall();
-            switch (expansionState) {
-              case ExpansionState.START_EXPANDING:
-                if (call != null) {
-                  Logger.get(context)
-                      .logCallImpression(
-                          DialerImpression.Type.BUBBLE_PRIMARY_BUTTON_EXPAND,
-                          call.getUniqueCallId(),
-                          call.getTimeAddedMs());
-                } else {
-                  Logger.get(context)
-                      .logImpression(DialerImpression.Type.BUBBLE_PRIMARY_BUTTON_EXPAND);
-                }
-                break;
-              case ExpansionState.START_COLLAPSING:
-                if (call != null) {
-                  Logger.get(context)
-                      .logCallImpression(
-                          DialerImpression.Type.BUBBLE_COLLAPSE_BY_USER,
-                          call.getUniqueCallId(),
-                          call.getTimeAddedMs());
-                } else {
-                  Logger.get(context).logImpression(DialerImpression.Type.BUBBLE_COLLAPSE_BY_USER);
-                }
-                break;
-              default:
-                break;
-            }
-          }
-        });
     returnToCallBubble.show();
     return returnToCallBubble;
   }
diff --git a/java/com/android/incallui/PhoneLookupHistoryRecorder.java b/java/com/android/incallui/PhoneLookupHistoryRecorder.java
new file mode 100644
index 0000000..2632e65
--- /dev/null
+++ b/java/com/android/incallui/PhoneLookupHistoryRecorder.java
@@ -0,0 +1,116 @@
+/*
+ * 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.incallui;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.Call;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SubscriptionInfo;
+import android.text.TextUtils;
+import com.android.dialer.buildtype.BuildType;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutors;
+import com.android.dialer.location.CountryDetector;
+import com.android.dialer.phonelookup.PhoneLookupComponent;
+import com.android.dialer.phonelookup.PhoneLookupInfo;
+import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.incallui.util.TelecomCallUtil;
+import com.google.common.base.Optional;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.util.Locale;
+
+/**
+ * Fetches the current {@link PhoneLookupInfo} for the provided call and writes it to the
+ * PhoneLookupHistory.
+ */
+final class PhoneLookupHistoryRecorder {
+
+  /**
+   * If the new UI is enabled, fetches the current {@link PhoneLookupInfo} for the provided call and
+   * writes it to the PhoneLookupHistory. Otherwise does nothing.
+   */
+  static void recordPhoneLookupInfo(Context appContext, Call call) {
+    if (!(BuildType.get() == BuildType.BUGFOOD || LogUtil.isDebugEnabled())) {
+      return;
+    }
+    ListenableFuture<PhoneLookupInfo> future =
+        PhoneLookupComponent.get(appContext).phoneLookup().lookup(call);
+    Futures.addCallback(
+        future,
+        new FutureCallback<PhoneLookupInfo>() {
+          @Override
+          public void onSuccess(@Nullable PhoneLookupInfo result) {
+            Assert.checkArgument(result != null);
+            Optional<String> normalizedNumber = getNormalizedNumber(appContext, call);
+            if (!normalizedNumber.isPresent()) {
+              LogUtil.w("PhoneLookupHistoryRecorder.onSuccess", "couldn't get a number");
+              return;
+            }
+            ContentValues contentValues = new ContentValues();
+            contentValues.put(PhoneLookupHistory.PHONE_LOOKUP_INFO, result.toByteArray());
+            contentValues.put(PhoneLookupHistory.LAST_MODIFIED, System.currentTimeMillis());
+            appContext
+                .getContentResolver()
+                .update(
+                    PhoneLookupHistory.CONTENT_URI
+                        .buildUpon()
+                        .appendEncodedPath(normalizedNumber.get())
+                        .build(),
+                    contentValues,
+                    null,
+                    null);
+          }
+
+          @Override
+          public void onFailure(Throwable t) {
+            // TODO(zachh): Consider how to best handle this; take measures to repair call log?
+            LogUtil.w(
+                "PhoneLookupHistoryRecorder.onFailure", "could not write PhoneLookupHistory", t);
+          }
+        },
+        DialerExecutors.getLowPriorityThreadPool(appContext));
+  }
+
+  private static Optional<String> getNormalizedNumber(Context appContext, Call call) {
+    PhoneAccountHandle phoneAccountHandle = call.getDetails().getAccountHandle();
+    Optional<SubscriptionInfo> subscriptionInfo =
+        TelecomUtil.getSubscriptionInfo(appContext, phoneAccountHandle);
+    String countryCode =
+        subscriptionInfo.isPresent()
+            ? subscriptionInfo.get().getCountryIso()
+            : CountryDetector.getInstance(appContext).getCurrentCountryIso();
+    if (countryCode == null) {
+      LogUtil.w(
+          "PhoneLookupHistoryRecorder.getNormalizedNumber",
+          "couldn't find a country code for call");
+      countryCode = "US";
+    }
+    String rawNumber = TelecomCallUtil.getNumber(call);
+    if (TextUtils.isEmpty(rawNumber)) {
+      return Optional.absent();
+    }
+    String normalizedNumber =
+        PhoneNumberUtils.formatNumberToE164(rawNumber, countryCode.toUpperCase(Locale.US));
+    return normalizedNumber == null ? Optional.of(rawNumber) : Optional.of(normalizedNumber);
+  }
+}
diff --git a/java/com/android/incallui/ReturnToCallController.java b/java/com/android/incallui/ReturnToCallController.java
index 5f4cc5f..58d8688 100644
--- a/java/com/android/incallui/ReturnToCallController.java
+++ b/java/com/android/incallui/ReturnToCallController.java
@@ -84,6 +84,7 @@
   }
 
   public void tearDown() {
+    hide();
     InCallPresenter.getInstance().removeInCallUiListener(this);
     CallList.getInstance().removeListener(this);
     AudioModeProvider.getInstance().removeListener(this);
diff --git a/java/com/android/newbubble/NewBubble.java b/java/com/android/newbubble/NewBubble.java
index e690f4b..ef3a971 100644
--- a/java/com/android/newbubble/NewBubble.java
+++ b/java/com/android/newbubble/NewBubble.java
@@ -17,12 +17,15 @@
 package com.android.newbubble;
 
 import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.annotation.SuppressLint;
 import android.app.PendingIntent.CanceledException;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Path;
 import android.graphics.PixelFormat;
 import android.graphics.drawable.Animatable;
 import android.graphics.drawable.Drawable;
@@ -51,10 +54,16 @@
 import android.view.WindowManager.LayoutParams;
 import android.view.animation.AnticipateInterpolator;
 import android.view.animation.OvershootInterpolator;
+import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.TextView;
 import android.widget.ViewAnimator;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.DialerImpression;
+import com.android.dialer.logging.Logger;
 import com.android.dialer.util.DrawableConverter;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
 import com.android.newbubble.NewBubbleInfo.Action;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -96,11 +105,14 @@
   private CharSequence textAfterShow;
   private int collapseEndAction;
 
-  @VisibleForTesting ViewHolder viewHolder;
+  ViewHolder viewHolder;
   private ViewPropertyAnimator collapseAnimation;
   private Integer overrideGravity;
   private ViewPropertyAnimator exitAnimator;
 
+  private int leftBoundary;
+  private int savedYPosition = -1;
+
   private final Runnable collapseRunnable =
       new Runnable() {
         @Override
@@ -110,17 +122,11 @@
             // Always reset here since text shouldn't keep showing.
             hideAndReset();
           } else {
-            doResize(
-                () ->
-                    viewHolder
-                        .getPrimaryButton()
-                        .setDisplayedChild(ViewHolder.CHILD_INDEX_AVATAR_AND_ICON));
+            viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_AVATAR_AND_ICON);
           }
         }
       };
 
-  private BubbleExpansionStateListener bubbleExpansionStateListener;
-
   /** Type of action after bubble collapse */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE})
@@ -206,15 +212,20 @@
     windowManager = context.getSystemService(WindowManager.class);
 
     viewHolder = new ViewHolder(context);
+
+    leftBoundary =
+        context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal)
+            - context
+                .getResources()
+                .getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal);
   }
 
   /** Expands the main bubble menu. */
   public void expand(boolean isUserAction) {
-    if (bubbleExpansionStateListener != null) {
-      bubbleExpansionStateListener.onBubbleExpansionStateChanged(
-          ExpansionState.START_EXPANDING, isUserAction);
+    if (isUserAction) {
+      logBasicOrCallImpression(DialerImpression.Type.BUBBLE_PRIMARY_BUTTON_EXPAND);
     }
-    doResize(() -> viewHolder.setDrawerVisibility(View.VISIBLE));
+    viewHolder.setDrawerVisibility(View.INVISIBLE);
     View expandedView = viewHolder.getExpandedView();
     expandedView
         .getViewTreeObserver()
@@ -222,13 +233,62 @@
             new OnPreDrawListener() {
               @Override
               public boolean onPreDraw() {
-                // Animate expanded view to move from above primary button to its final position
+                // Move the whole bubble up so that expanded view is still in screen
+                int moveUpDistance = viewHolder.getMoveUpDistance();
+                if (moveUpDistance != 0) {
+                  savedYPosition = windowParams.y;
+                }
+
+                // Calculate the move-to-middle distance
+                int deltaX =
+                    (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX();
+                float k = (float) moveUpDistance / deltaX;
+                if (isDrawingFromRight()) {
+                  deltaX = -deltaX;
+                }
+
+                // Do X-move and Y-move together
+
+                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(
+                    new AnimatorListener() {
+                      @Override
+                      public void onAnimationEnd(Animator animation) {
+                        // Show expanded view
+                        expandedView.setVisibility(View.VISIBLE);
+                        expandedView.setTranslationY(-expandedView.getHeight());
+                        expandedView
+                            .animate()
+                            .setInterpolator(new LinearOutSlowInInterpolator())
+                            .translationY(0);
+                      }
+
+                      @Override
+                      public void onAnimationStart(Animator animation) {}
+
+                      @Override
+                      public void onAnimationCancel(Animator animation) {}
+
+                      @Override
+                      public void onAnimationRepeat(Animator animation) {}
+                    });
+                animator.start();
+
                 expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
-                expandedView.setTranslationY(-viewHolder.getRoot().getHeight());
-                expandedView
-                    .animate()
-                    .setInterpolator(new LinearOutSlowInInterpolator())
-                    .translationY(0);
                 return false;
               }
             });
@@ -236,6 +296,115 @@
     expanded = true;
   }
 
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  public void startCollapse(
+      @CollapseEnd int endAction, boolean isUserAction, boolean shouldRecoverYPosition) {
+    View expandedView = viewHolder.getExpandedView();
+    if (expandedView.getVisibility() != View.VISIBLE || collapseAnimation != null) {
+      // Drawer is already collapsed or animation is running.
+      return;
+    }
+
+    overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT;
+    setFocused(false);
+
+    if (collapseEndAction == CollapseEnd.NOTHING) {
+      collapseEndAction = endAction;
+    }
+    if (isUserAction && collapseEndAction == CollapseEnd.NOTHING) {
+      logBasicOrCallImpression(DialerImpression.Type.BUBBLE_COLLAPSE_BY_USER);
+    }
+    // Animate expanded view to move from its position to above primary button and hide
+    collapseAnimation =
+        expandedView
+            .animate()
+            .translationY(-expandedView.getHeight())
+            .setInterpolator(new FastOutLinearInInterpolator())
+            .withEndAction(
+                () -> {
+                  collapseAnimation = null;
+                  expanded = false;
+
+                  if (textShowing) {
+                    // Will do resize once the text is done.
+                    return;
+                  }
+
+                  // 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 AnimatorListener() {
+                        @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();
+                        }
+
+                        @Override
+                        public void onAnimationStart(Animator animation) {}
+
+                        @Override
+                        public void onAnimationCancel(Animator animation) {}
+
+                        @Override
+                        public void onAnimationRepeat(Animator animation) {}
+                      });
+                  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);
+                });
+  }
+
   /**
    * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is
    * already showing this method does nothing.
@@ -269,8 +438,7 @@
                   | LayoutParams.FLAG_LAYOUT_NO_LIMITS,
               PixelFormat.TRANSLUCENT);
       windowParams.gravity = Gravity.TOP | Gravity.LEFT;
-      windowParams.x =
-          context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_horizontal);
+      windowParams.x = leftBoundary;
       windowParams.y = currentInfo.getStartingYPosition();
       windowParams.height = LayoutParams.WRAP_CONTENT;
       windowParams.width = LayoutParams.WRAP_CONTENT;
@@ -392,7 +560,8 @@
   public void showText(@NonNull CharSequence text) {
     textShowing = true;
     if (expanded) {
-      startCollapse(CollapseEnd.NOTHING, false);
+      startCollapse(
+          CollapseEnd.NOTHING, false /* isUserAction */, false /* shouldRecoverYPosition */);
       doShowText(text);
     } else {
       // Need to transition from old bounds to new bounds manually
@@ -409,68 +578,65 @@
         return;
       }
 
-      doResize(
-          () -> {
-            doShowText(text);
-            // Hide the text so we can animate it in
-            viewHolder.getPrimaryText().setAlpha(0);
+      doShowText(text);
+      // Hide the text so we can animate it in
+      viewHolder.getPrimaryText().setAlpha(0);
 
-            ViewAnimator primaryButton = viewHolder.getPrimaryButton();
-            // Cancel the automatic transition scheduled in doShowText
-            TransitionManager.endTransitions((ViewGroup) primaryButton.getParent());
-            primaryButton
-                .getViewTreeObserver()
-                .addOnPreDrawListener(
-                    new OnPreDrawListener() {
-                      @Override
-                      public boolean onPreDraw() {
-                        primaryButton.getViewTreeObserver().removeOnPreDrawListener(this);
+      ViewAnimator primaryButton = viewHolder.getPrimaryButton();
+      // Cancel the automatic transition scheduled in doShowText
+      TransitionManager.endTransitions((ViewGroup) primaryButton.getParent());
+      primaryButton
+          .getViewTreeObserver()
+          .addOnPreDrawListener(
+              new OnPreDrawListener() {
+                @Override
+                public boolean onPreDraw() {
+                  primaryButton.getViewTreeObserver().removeOnPreDrawListener(this);
 
-                        // Prepare and capture end values, always use the size of primaryText since
-                        // its invisibility makes primaryButton smaller than expected
-                        TransitionValues endValues = new TransitionValues();
-                        endValues.values.put(
-                            NewChangeOnScreenBounds.PROPNAME_WIDTH,
-                            viewHolder.getPrimaryText().getWidth());
-                        endValues.values.put(
-                            NewChangeOnScreenBounds.PROPNAME_HEIGHT,
-                            viewHolder.getPrimaryText().getHeight());
-                        endValues.view = primaryButton;
-                        transition.addTarget(endValues.view);
-                        transition.captureEndValues(endValues);
+                  // Prepare and capture end values, always use the size of primaryText since
+                  // its invisibility makes primaryButton smaller than expected
+                  TransitionValues endValues = new TransitionValues();
+                  endValues.values.put(
+                      NewChangeOnScreenBounds.PROPNAME_WIDTH,
+                      viewHolder.getPrimaryText().getWidth());
+                  endValues.values.put(
+                      NewChangeOnScreenBounds.PROPNAME_HEIGHT,
+                      viewHolder.getPrimaryText().getHeight());
+                  endValues.view = primaryButton;
+                  transition.addTarget(endValues.view);
+                  transition.captureEndValues(endValues);
 
-                        // animate the primary button bounds change
-                        Animator bounds =
-                            transition.createAnimator(primaryButton, startValues, endValues);
+                  // animate the primary button bounds change
+                  Animator bounds =
+                      transition.createAnimator(primaryButton, startValues, endValues);
 
-                        // Animate the text in
-                        Animator alpha =
-                            ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f);
+                  // Animate the text in
+                  Animator alpha =
+                      ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f);
 
-                        AnimatorSet set = new AnimatorSet();
-                        set.play(bounds).before(alpha);
-                        set.start();
-                        return false;
-                      }
-                    });
-          });
+                  AnimatorSet set = new AnimatorSet();
+                  set.play(bounds).before(alpha);
+                  set.start();
+                  return false;
+                }
+              });
     }
     handler.removeCallbacks(collapseRunnable);
     handler.postDelayed(collapseRunnable, SHOW_TEXT_DURATION_MILLIS);
   }
 
-  public void setBubbleExpansionStateListener(
-      BubbleExpansionStateListener bubbleExpansionStateListener) {
-    this.bubbleExpansionStateListener = bubbleExpansionStateListener;
-  }
-
   @Nullable
   Integer getGravityOverride() {
     return overrideGravity;
   }
 
   void onMoveStart() {
-    startCollapse(CollapseEnd.NOTHING, true);
+    if (viewHolder.getExpandedView().getVisibility() == View.VISIBLE) {
+      viewHolder.setDrawerVisibility(View.INVISIBLE);
+    }
+    expanded = false;
+    savedYPosition = -1;
+
     viewHolder
         .getPrimaryButton()
         .animate()
@@ -482,11 +648,6 @@
 
   void onMoveFinish() {
     viewHolder.getPrimaryButton().animate().translationZ(0);
-    // If it's GONE, no resize is necessary. If it's VISIBLE, it will get cleaned up when the
-    // collapse animation finishes
-    if (viewHolder.getExpandedView().getVisibility() == View.INVISIBLE) {
-      doResize(null);
-    }
   }
 
   void primaryButtonClick() {
@@ -494,12 +655,22 @@
       return;
     }
     if (expanded) {
-      startCollapse(CollapseEnd.NOTHING, true);
+      startCollapse(
+          CollapseEnd.NOTHING, true /* isUserAction */, true /* shouldRecoverYPosition */);
     } else {
       expand(true);
     }
   }
 
+  void onLeftRightSwitch(boolean onRight) {
+    // Set layout direction so the small icon is not partially hidden.
+    View primaryIcon = viewHolder.getPrimaryIcon();
+    int newGravity = (onRight ? Gravity.LEFT : Gravity.RIGHT) | Gravity.BOTTOM;
+    FrameLayout.LayoutParams layoutParams =
+        new FrameLayout.LayoutParams(primaryIcon.getWidth(), primaryIcon.getHeight(), newGravity);
+    primaryIcon.setLayoutParams(layoutParams);
+  }
+
   LayoutParams getWindowParams() {
     return windowParams;
   }
@@ -532,7 +703,7 @@
     }
 
     if (expanded) {
-      startCollapse(CollapseEnd.HIDE, false);
+      startCollapse(CollapseEnd.HIDE, false /* isUserAction */, false /* shouldRecoverYPosition */);
       return;
     }
 
@@ -618,34 +789,39 @@
     }
   }
 
-  private void doResize(@Nullable Runnable operation) {
-    // If we're resizing on the right side of the screen, there is an implicit move operation
-    // necessary. The WindowManager does not sync the move and resize operations, so serious jank
-    // would occur. To fix this, instead of resizing the window, we create a new one and destroy
-    // the old one. There is a short delay before destroying the old view to ensure the new one has
-    // had time to draw.
+  /**
+   * Create a new ViewHolder object to replace the old one.It only happens when not moving and
+   * collapsed.
+   */
+  void replaceViewHolder() {
+    LogUtil.enterBlock("NewBubble.replaceViewHolder");
     ViewHolder oldViewHolder = viewHolder;
-    if (isDrawingFromRight()) {
-      viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext());
-      update();
-      viewHolder
-          .getPrimaryButton()
-          .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild());
-      viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText());
-    }
 
-    if (operation != null) {
-      operation.run();
-    }
+    // Create a new ViewHolder and copy needed info.
+    viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext());
+    viewHolder
+        .getPrimaryButton()
+        .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild());
+    viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText());
 
-    if (isDrawingFromRight()) {
-      swapViewHolders(oldViewHolder);
-    }
-  }
+    int size = context.getResources().getDimensionPixelSize(R.dimen.bubble_small_icon_size);
+    viewHolder
+        .getPrimaryIcon()
+        .setLayoutParams(
+            new FrameLayout.LayoutParams(
+                size,
+                size,
+                Gravity.BOTTOM | (isDrawingFromRight() ? Gravity.LEFT : Gravity.RIGHT)));
 
-  private void swapViewHolders(ViewHolder oldViewHolder) {
+    update();
+
+    // Add new view at its horizontal boundary
     ViewGroup root = viewHolder.getRoot();
+    windowParams.x = leftBoundary;
+    windowParams.gravity = Gravity.TOP | (isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT);
     windowManager.addView(root, windowParams);
+
+    // Remove the old view after delay
     root.getViewTreeObserver()
         .addOnPreDrawListener(
             new OnPreDrawListener() {
@@ -661,63 +837,8 @@
             });
   }
 
-  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  public void startCollapse(@CollapseEnd int endAction, boolean isUserAction) {
-    View expandedView = viewHolder.getExpandedView();
-    if (expandedView.getVisibility() != View.VISIBLE || collapseAnimation != null) {
-      // Drawer is already collapsed or animation is running.
-      return;
-    }
-
-    overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT;
-    setFocused(false);
-
-    if (collapseEndAction == CollapseEnd.NOTHING) {
-      collapseEndAction = endAction;
-    }
-    if (bubbleExpansionStateListener != null && collapseEndAction == CollapseEnd.NOTHING) {
-      bubbleExpansionStateListener.onBubbleExpansionStateChanged(
-          ExpansionState.START_COLLAPSING, isUserAction);
-    }
-    // Animate expanded view to move from its position to above primary button and hide
-    collapseAnimation =
-        expandedView
-            .animate()
-            .translationY(-viewHolder.getRoot().getHeight())
-            .setInterpolator(new FastOutLinearInInterpolator())
-            .withEndAction(
-                () -> {
-                  collapseAnimation = null;
-                  expanded = false;
-
-                  if (textShowing) {
-                    // Will do resize once the text is done.
-                    return;
-                  }
-
-                  // Hide the drawer and resize if possible.
-                  viewHolder.setDrawerVisibility(View.INVISIBLE);
-                  if (!viewHolder.isMoving() || !isDrawingFromRight()) {
-                    doResize(() -> viewHolder.setDrawerVisibility(View.GONE));
-                  }
-
-                  // 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);
-                });
+  int getDrawerVisibility() {
+    return viewHolder.getExpandedView().getVisibility();
   }
 
   private boolean isDrawingFromRight() {
@@ -741,6 +862,16 @@
     updatePrimaryIconAnimation();
   }
 
+  private void logBasicOrCallImpression(DialerImpression.Type impressionType) {
+    DialerCall call = CallList.getInstance().getActiveOrBackgroundCall();
+    if (call != null) {
+      Logger.get(context)
+          .logCallImpression(impressionType, call.getUniqueCallId(), call.getTimeAddedMs());
+    } else {
+      Logger.get(context).logImpression(impressionType);
+    }
+  }
+
   @VisibleForTesting
   class ViewHolder {
 
@@ -779,7 +910,8 @@
       root.setOnBackPressedListener(
           () -> {
             if (visibility == Visibility.SHOWING && expanded) {
-              startCollapse(CollapseEnd.NOTHING, true);
+              startCollapse(
+                  CollapseEnd.NOTHING, true /* isUserAction */, true /* shouldRecoverYPosition */);
               return true;
             }
             return false;
@@ -794,7 +926,8 @@
       root.setOnTouchListener(
           (v, event) -> {
             if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
-              startCollapse(CollapseEnd.NOTHING, true);
+              startCollapse(
+                  CollapseEnd.NOTHING, true /* isUserAction */, true /* shouldRecoverYPosition */);
               return true;
             }
             return false;
@@ -812,6 +945,16 @@
       moveHandler.setClickable(clickable);
     }
 
+    public int getMoveUpDistance() {
+      int deltaAllowed =
+          expandedView.getHeight()
+              - context
+                      .getResources()
+                      .getDimensionPixelOffset(R.dimen.bubble_button_padding_vertical)
+                  * 2;
+      return moveHandler.getMoveUpDistance(deltaAllowed);
+    }
+
     public ViewGroup getRoot() {
       return root;
     }
@@ -864,9 +1007,4 @@
       moveHandler.undoGravityOverride();
     }
   }
-
-  /** Listener for bubble expansion state change. */
-  public interface BubbleExpansionStateListener {
-    void onBubbleExpansionStateChanged(@ExpansionState int expansionState, boolean isUserAction);
-  }
 }
diff --git a/java/com/android/newbubble/NewMoveHandler.java b/java/com/android/newbubble/NewMoveHandler.java
index 189ad84..9cb1f1e 100644
--- a/java/com/android/newbubble/NewMoveHandler.java
+++ b/java/com/android/newbubble/NewMoveHandler.java
@@ -48,6 +48,8 @@
   private final int maxX;
   private final int maxY;
   private final int bubbleSize;
+  private final int bubbleShadowPaddingHorizontal;
+  private final int bubbleExpandedViewWidth;
   private final float touchSlopSquared;
 
   private boolean clickable = true;
@@ -70,8 +72,14 @@
         @Override
         public float getValue(LayoutParams windowParams) {
           int realX = windowParams.x;
-          realX = realX + bubbleSize / 2;
+          // Get bubble center position from real position
+          if (bubble.getDrawerVisibility() == View.INVISIBLE) {
+            realX += bubbleExpandedViewWidth / 2 + bubbleShadowPaddingHorizontal * 2;
+          } else {
+            realX += bubbleSize / 2 + bubbleShadowPaddingHorizontal;
+          }
           if (relativeToRight(windowParams)) {
+            // If gravity is right, get distant from bubble center position to screen right edge
             int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
             realX = displayWidth - realX;
           }
@@ -88,12 +96,19 @@
           } else {
             onRight = (gravityOverride & Gravity.RIGHT) == Gravity.RIGHT;
           }
-          int centeringOffset = bubbleSize / 2;
+          // Get real position from bubble center position
+          int centeringOffset;
+          if (bubble.getDrawerVisibility() == View.INVISIBLE) {
+            centeringOffset = bubbleExpandedViewWidth / 2 + bubbleShadowPaddingHorizontal * 2;
+          } else {
+            centeringOffset = bubbleSize / 2 + bubbleShadowPaddingHorizontal;
+          }
           windowParams.x =
               (int) (onRight ? (displayWidth - value - centeringOffset) : value - centeringOffset);
           windowParams.gravity = Gravity.TOP | (onRight ? Gravity.RIGHT : Gravity.LEFT);
           if (bubble.isVisible()) {
             windowManager.updateViewLayout(bubble.getRootView(), windowParams);
+            bubble.onLeftRightSwitch(onRight);
           }
         }
       };
@@ -120,8 +135,13 @@
     windowManager = context.getSystemService(WindowManager.class);
 
     bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
+    bubbleShadowPaddingHorizontal =
+        context.getResources().getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal);
+    bubbleExpandedViewWidth =
+        context.getResources().getDimensionPixelSize(R.dimen.bubble_expanded_width);
+    // The following value is based on bubble center
     minX =
-        context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_horizontal)
+        context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal)
             + bubbleSize / 2;
     minY =
         context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_vertical)
@@ -156,6 +176,12 @@
     moveYAnimation.animateToFinalPosition(yProperty.getValue(bubble.getWindowParams()));
   }
 
+  public int getMoveUpDistance(int deltaAllowed) {
+    int currentY = (int) yProperty.getValue(bubble.getWindowParams());
+    int currentDelta = maxY - currentY;
+    return currentDelta >= deltaAllowed ? 0 : deltaAllowed - currentDelta;
+  }
+
   @Override
   public boolean onTouch(View v, MotionEvent event) {
     float eventX = event.getRawX();
@@ -222,6 +248,14 @@
       moveXAnimation = new SpringAnimation(bubble.getWindowParams(), xProperty);
       moveXAnimation.setSpring(new SpringForce());
       moveXAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
+      // Moving when expanded makes expanded view INVISIBLE, and the whole view is not at the
+      // boundary. It's time to create a viewHolder.
+      moveXAnimation.addEndListener(
+          (animation, canceled, value, velocity) -> {
+            if (!isMoving && bubble.getDrawerVisibility() == View.INVISIBLE) {
+              bubble.replaceViewHolder();
+            }
+          });
     }
 
     if (moveYAnimation == null) {
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 8cac982..8d47716 100644
--- a/java/com/android/newbubble/res/layout/new_bubble_base.xml
+++ b/java/com/android/newbubble/res/layout/new_bubble_base.xml
@@ -19,7 +19,8 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:clipChildren="false"
+    android:clipChildren="true"
+    android:clipToPadding="false"
     tools:theme="@style/Theme.AppCompat">
   <RelativeLayout
       android:id="@+id/bubble_primary_container"
@@ -41,7 +42,8 @@
         android:measureAllChildren="false"
         android:elevation="@dimen/bubble_elevation"
         tools:backgroundTint="#FF0000AA">
-      <RelativeLayout
+      <FrameLayout
+          android:id="@+id/bubble_icon_container"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content">
         <ImageView
@@ -53,8 +55,7 @@
             android:id="@+id/bubble_icon_primary"
             android:layout_width="@dimen/bubble_small_icon_size"
             android:layout_height="@dimen/bubble_small_icon_size"
-            android:layout_alignBottom="@id/bubble_icon_avatar"
-            android:layout_alignEnd="@id/bubble_icon_avatar"
+            android:layout_gravity="bottom|right"
             android:padding="@dimen/bubble_small_icon_padding"
             android:tint="@android:color/white"
             android:tintMode="src_in"
@@ -62,7 +63,7 @@
             android:measureAllChildren="false"
             tools:backgroundTint="#FF0000AA"
             tools:src="@android:drawable/ic_btn_speak_now"/>
-      </RelativeLayout>
+      </FrameLayout>
       <TextView
           android:id="@+id/bubble_text"
           android:layout_width="wrap_content"
@@ -75,67 +76,77 @@
           tools:text="Call ended"/>
     </ViewAnimator>
   </RelativeLayout>
+  <!-- The RelativeLayout below serves as boundary for @id/bubble_expanded_layout during animation -->
   <RelativeLayout
-      android:id="@+id/bubble_expanded_layout"
-      android:layout_width="@dimen/bubble_expanded_width"
+      android:layout_width="wrap_content"
       android:layout_height="wrap_content"
-      android:layout_below="@id/bubble_primary_container"
-      android:layout_marginStart="@dimen/bubble_shadow_padding_size_horizontal_double"
-      android:layout_marginEnd="@dimen/bubble_shadow_padding_size_horizontal_double"
       android:layout_marginTop="@dimen/bubble_shadow_padding_size_vertical_minus"
-      android:layout_marginBottom="@dimen/bubble_shadow_padding_size_vertical"
-      android:visibility="gone"
-      tools:visibility="visible">
+      android:clipChildren="true"
+      android:clipToPadding="false"
+      android:layout_below="@id/bubble_primary_container">
     <RelativeLayout
-        android:id="@+id/bubble_triangle"
-        android:layout_width="12dp"
-        android:layout_height="12dp"
-        android:layout_marginTop="7dp"
-        android:layout_marginBottom="-6dp"
-        android:layout_centerHorizontal="true"
-        android:background="@color/background_dialer_white"
-        android:elevation="@dimen/bubble_expanded_elevation"
-        android:rotation="45">
-    </RelativeLayout>
-    <RelativeLayout
-        android:layout_width="match_parent"
+        android:id="@+id/bubble_expanded_layout"
+        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_below="@id/bubble_triangle"
+        android:paddingStart="@dimen/bubble_shadow_padding_size_horizontal_double"
+        android:paddingEnd="@dimen/bubble_shadow_padding_size_horizontal_double"
+        android:paddingBottom="@dimen/bubble_shadow_padding_size_vertical"
+        android:clipChildren="false"
         android:clipToPadding="false"
-        android:background="@drawable/bubble_background_with_radius"
-        android:elevation="@dimen/bubble_expanded_elevation"
-        android:layoutDirection="inherit">
-      <com.android.newbubble.NewCheckableButton
-          android:id="@+id/bubble_button_full_screen"
-          android:layout_marginTop="8dp"
-          android:textColor="@color/bubble_button_color_grey"
-          android:background="@drawable/bubble_ripple_pill_up"
-          android:drawableTint="@color/bubble_button_color_grey"
-          style="@style/CheckableButton"/>
-      <com.android.newbubble.NewCheckableButton
-          android:id="@+id/bubble_button_mute"
-          android:layout_below="@id/bubble_button_full_screen"
-          android:textColor="@color/bubble_button_color_grey"
+        android:visibility="gone"
+        tools:visibility="visible">
+      <RelativeLayout
+          android:id="@+id/bubble_triangle"
+          android:layout_width="12dp"
+          android:layout_height="12dp"
+          android:layout_marginTop="7dp"
+          android:layout_marginBottom="-6dp"
+          android:layout_centerHorizontal="true"
           android:background="@color/background_dialer_white"
-          android:drawableTint="@color/bubble_button_color_grey"
-          style="@style/CheckableButtonWithSelectableItemBackground"/>
-      <com.android.newbubble.NewCheckableButton
-          android:id="@+id/bubble_button_audio_route"
-          android:layout_below="@id/bubble_button_mute"
-          android:textColor="@color/bubble_button_color_grey"
-          android:background="@color/background_dialer_white"
-          android:drawableTint="@color/bubble_button_color_grey"
-          style="@style/CheckableButtonWithSelectableItemBackground"/>
-      <com.android.newbubble.NewCheckableButton
-          android:id="@+id/bubble_button_end_call"
-          android:layout_below="@id/bubble_button_audio_route"
-          android:layout_marginTop="@dimen/bubble_expanded_separator_height"
-          android:textColor="@color/bubble_button_color_white"
-          android:background="@drawable/bubble_pill_down"
-          android:backgroundTint="@color/dialer_end_call_button_color"
-          android:foreground="?attr/selectableItemBackground"
-          android:drawableTint="@color/bubble_button_color_white"
-          style="@style/CheckableButton"/>
+          android:elevation="@dimen/bubble_expanded_elevation"
+          android:rotation="45">
+      </RelativeLayout>
+      <RelativeLayout
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:layout_below="@id/bubble_triangle"
+          android:background="@drawable/bubble_background_with_radius"
+          android:elevation="@dimen/bubble_expanded_elevation"
+          android:clipChildren="false"
+          android:clipToPadding="false"
+          android:layoutDirection="inherit">
+        <com.android.newbubble.NewCheckableButton
+            android:id="@+id/bubble_button_full_screen"
+            android:layout_marginTop="8dp"
+            android:textColor="@color/bubble_button_color_grey"
+            android:background="@drawable/bubble_ripple_pill_up"
+            android:drawableTint="@color/bubble_button_color_grey"
+            style="@style/CheckableButton"/>
+        <com.android.newbubble.NewCheckableButton
+            android:id="@+id/bubble_button_mute"
+            android:layout_below="@id/bubble_button_full_screen"
+            android:textColor="@color/bubble_button_color_grey"
+            android:background="@color/background_dialer_white"
+            android:drawableTint="@color/bubble_button_color_grey"
+            style="@style/CheckableButtonWithSelectableItemBackground"/>
+        <com.android.newbubble.NewCheckableButton
+            android:id="@+id/bubble_button_audio_route"
+            android:layout_below="@id/bubble_button_mute"
+            android:textColor="@color/bubble_button_color_grey"
+            android:background="@color/background_dialer_white"
+            android:drawableTint="@color/bubble_button_color_grey"
+            style="@style/CheckableButtonWithSelectableItemBackground"/>
+        <com.android.newbubble.NewCheckableButton
+            android:id="@+id/bubble_button_end_call"
+            android:layout_below="@id/bubble_button_audio_route"
+            android:layout_marginTop="@dimen/bubble_expanded_separator_height"
+            android:textColor="@color/bubble_button_color_white"
+            android:background="@drawable/bubble_pill_down"
+            android:backgroundTint="@color/dialer_end_call_button_color"
+            android:foreground="?attr/selectableItemBackground"
+            android:drawableTint="@color/bubble_button_color_white"
+            style="@style/CheckableButton"/>
+      </RelativeLayout>
     </RelativeLayout>
   </RelativeLayout>
 </RelativeLayout>
diff --git a/java/com/android/newbubble/res/values/values.xml b/java/com/android/newbubble/res/values/values.xml
index 6dda61d..71f813a 100644
--- a/java/com/android/newbubble/res/values/values.xml
+++ b/java/com/android/newbubble/res/values/values.xml
@@ -24,8 +24,11 @@
   <dimen name="bubble_button_icon_padding">16dp</dimen>
   <dimen name="bubble_button_padding_vertical">12dp</dimen>
   <dimen name="bubble_button_padding_horizontal">16dp</dimen>
-  <dimen name="bubble_safe_margin_horizontal">-16dp</dimen>
-  <dimen name="bubble_safe_margin_vertical">64dp</dimen>
+
+  <dimen name="bubble_off_screen_size_horizontal">-4dp</dimen>
+  <!-- 64dp - 16dp(bubble_shadow_padding_size_vertical) -->
+  <dimen name="bubble_safe_margin_vertical">48dp</dimen>
+
   <dimen name="bubble_shadow_padding_size_vertical">16dp</dimen>
   <dimen name="bubble_shadow_padding_size_vertical_minus">-16dp</dimen>
   <dimen name="bubble_shadow_padding_size_horizontal">12dp</dimen>
diff --git a/java/com/android/voicemail/impl/ActivationTask.java b/java/com/android/voicemail/impl/ActivationTask.java
index 29c91e0..83f2fd8 100644
--- a/java/com/android/voicemail/impl/ActivationTask.java
+++ b/java/com/android/voicemail/impl/ActivationTask.java
@@ -61,7 +61,7 @@
   private static final int RETRY_TIMES = 4;
   private static final int RETRY_INTERVAL_MILLIS = 5_000;
 
-  private static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle";
+  @VisibleForTesting static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle";
 
   private final RetryPolicy mRetryPolicy;
 
@@ -168,7 +168,8 @@
     }
     VvmLog.i(TAG, "VVM content provider configured - " + helper.getVvmType());
 
-    if (VvmAccountManager.isAccountActivated(getContext(), phoneAccountHandle)) {
+    if (mMessageData == null
+        && VvmAccountManager.isAccountActivated(getContext(), phoneAccountHandle)) {
       VvmLog.i(TAG, "Account is already activated");
       // The activated state might come from restored data, the filter still needs to be set up.
       helper.activateSmsFilter();
diff --git a/java/com/android/voicemail/impl/PreOMigrationHandler.java b/java/com/android/voicemail/impl/PreOMigrationHandler.java
index 3ec5e08..2c45471 100644
--- a/java/com/android/voicemail/impl/PreOMigrationHandler.java
+++ b/java/com/android/voicemail/impl/PreOMigrationHandler.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.os.Bundle;
+import android.support.annotation.VisibleForTesting;
 import android.support.annotation.WorkerThread;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.TelephonyManager;
@@ -49,7 +50,7 @@
   private static final String EXTRA_VOICEMAIL_SCRAMBLED_PIN_STRING =
       "android.telephony.extra.VOICEMAIL_SCRAMBLED_PIN_STRING";
 
-  private static final String PRE_O_MIGRATION_FINISHED = "pre_o_migration_finished";
+  @VisibleForTesting static final String PRE_O_MIGRATION_FINISHED = "pre_o_migration_finished";
 
   @WorkerThread
   public static void migrate(Context context, PhoneAccountHandle phoneAccountHandle) {