Merge "Rename the new bubble package name from "bubble" to "newbubble"."
diff --git a/Android.mk b/Android.mk
index e1a58ad..bb37068 100644
--- a/Android.mk
+++ b/Android.mk
@@ -39,10 +39,6 @@
 
 # Exclude build variants for now
 EXCLUDE_FILES += \
-	$(BASE_DIR)/dialer/buildtype/bugfood/BuildTypeAccessorImpl.java \
-	$(BASE_DIR)/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java \
-	$(BASE_DIR)/dialer/buildtype/fishfood/BuildTypeAccessorImpl.java \
-	$(BASE_DIR)/dialer/buildtype/test/BuildTypeAccessorImpl.java \
 	$(BASE_DIR)/dialer/constants/googledialer/ConstantsImpl.java \
 	$(BASE_DIR)/dialer/binary/google/GoogleStubDialerRootComponent.java \
 	$(BASE_DIR)/dialer/binary/google/GoogleStubDialerApplication.java \
@@ -156,6 +152,7 @@
 	com.android.incallui.telecomeventui \
 	com.android.incallui.video.impl \
 	com.android.incallui.video.protocol \
+	com.android.newbubble \
 	com.android.voicemail \
 	com.android.voicemail.impl \
 	com.android.voicemail.impl.configui \
diff --git a/assets/quantum/res/drawable/quantum_ic_fullscreen_vd_theme_24.xml b/assets/quantum/res/drawable/quantum_ic_fullscreen_vd_theme_24.xml
new file mode 100644
index 0000000..48e3c31
--- /dev/null
+++ b/assets/quantum/res/drawable/quantum_ic_fullscreen_vd_theme_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
+</vector>
diff --git a/java/com/android/contacts/common/MoreContactUtils.java b/java/com/android/contacts/common/MoreContactUtils.java
index 26241b3..9749cab 100644
--- a/java/com/android/contacts/common/MoreContactUtils.java
+++ b/java/com/android/contacts/common/MoreContactUtils.java
@@ -65,7 +65,7 @@
 
   // TODO: Move this to PhoneDataItem.shouldCollapse override
   private static boolean shouldCollapsePhoneNumbers(String number1, String number2) {
-    // Work around to address b/20724444. We want to distinguish between #555, *555 and 555.
+    // Work around to address a bug. We want to distinguish between #555, *555 and 555.
     // This makes no attempt to distinguish between 555 and 55*5, since 55*5 is an improbable
     // number. PhoneNumberUtil already distinguishes between 555 and 55#5.
     if (number1.contains("#") != number2.contains("#")
@@ -119,7 +119,7 @@
               // +14155551212
               //   4155551212
               //
-              // From b/7519057, case 2 needs to be equal.  But also that bug, case 3
+              // From a bug, case 2 needs to be equal.  But also that bug, case 3
               // below should not be equal.
               //
               // case 3)
diff --git a/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
index 172fb11..bb0d3c6 100644
--- a/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
+++ b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
@@ -24,7 +24,7 @@
 public class TelecomManagerCompat {
 
   // TODO(mdooley): remove once this is available in android.telecom.Call
-  // b/33779976
+  // a bug
   public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS =
       "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS";
 
diff --git a/java/com/android/contacts/common/list/AutoScrollListView.java b/java/com/android/contacts/common/list/AutoScrollListView.java
index 601abf5..8d6455f 100644
--- a/java/com/android/contacts/common/list/AutoScrollListView.java
+++ b/java/com/android/contacts/common/list/AutoScrollListView.java
@@ -116,7 +116,7 @@
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
     super.onLayout(changed, l, t, r, b);
 
-    // Workaround for b/31160338 and b/32778636.
+    // Workaround for a bug and a bug.
     if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N
         || android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
       layoutChildren();
diff --git a/java/com/android/contacts/common/list/ContactEntryListFragment.java b/java/com/android/contacts/common/list/ContactEntryListFragment.java
index 94551a8..c807ea8 100644
--- a/java/com/android/contacts/common/list/ContactEntryListFragment.java
+++ b/java/com/android/contacts/common/list/ContactEntryListFragment.java
@@ -825,7 +825,7 @@
   @Override
   public void onResume() {
     super.onResume();
-    // Restore the selection of the list view. See b/19982820.
+    // Restore the selection of the list view. See a bug.
     // This has to be done manually because if the list view has its emptyView set,
     // the scrolling state will be reset when clearPartitions() is called on the adapter.
     mListView.setSelectionFromTop(mListViewTopIndex, mListViewTopOffset);
diff --git a/java/com/android/contacts/common/res/drawable/back_arrow.xml b/java/com/android/contacts/common/res/drawable/back_arrow.xml
new file mode 100644
index 0000000..34fa3d7
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/back_arrow.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    android:autoMirrored="true"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
+</vector>
diff --git a/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml b/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml
index 2aa9772..9e9be95 100644
--- a/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml
+++ b/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<!-- layoutDirection set to ltr as a workaround to a framework bug (b/22010411) causing view with
+<!-- layoutDirection set to ltr as a workaround to a framework bug (a bug) causing view with
      layout_centerInParent inside a RelativeLayout to expand to screen width when RTL is active -->
 <RelativeLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/java/com/android/contacts/common/res/layout/search_bar_expanded.xml b/java/com/android/contacts/common/res/layout/search_bar_expanded.xml
index f0179ad..ccea3f7 100644
--- a/java/com/android/contacts/common/res/layout/search_bar_expanded.xml
+++ b/java/com/android/contacts/common/res/layout/search_bar_expanded.xml
@@ -28,7 +28,7 @@
     android:layout_centerVertical="true"
     android:background="?attr/selectableItemBackgroundBorderless"
     android:contentDescription="@string/action_menu_back_from_search"
-    android:src="@drawable/quantum_ic_arrow_back_vd_theme_24"
+    android:src="@drawable/back_arrow"
     android:tint="@color/contactscommon_actionbar_background_color"/>
 
   <EditText
diff --git a/java/com/android/dialer/about/res/raw/third_party_license_metadata b/java/com/android/dialer/about/res/raw/third_party_license_metadata
index b20ef89..49c2273 100755
--- a/java/com/android/dialer/about/res/raw/third_party_license_metadata
+++ b/java/com/android/dialer/about/res/raw/third_party_license_metadata
@@ -30,13 +30,13 @@
 321603:11358 OpenCensus
 332972:11358 Volley
 344341:10695 bubble
-355050:10402 gRPC Java
-365471:10173 libphonenumber
-375663:10699 shortcutbadger
-386378:16013 Android SDK
-402410:1096 Animal Sniffer
-403516:4771 Glide
-408299:1602 JSR 305
-409919:12847 jibercsclient
-422777:18982 mime4j
-441776:12847 rcsclientlib
+355050:11358 gRPC Java
+366427:10173 libphonenumber
+376619:10699 shortcutbadger
+387334:16013 Android SDK
+403366:1096 Animal Sniffer
+404472:4771 Glide
+409255:1602 JSR 305
+410875:12847 jibercsclient
+423733:18982 mime4j
+442732:12847 rcsclientlib
diff --git a/java/com/android/dialer/about/res/raw/third_party_licenses b/java/com/android/dialer/about/res/raw/third_party_licenses
index 443c510..64f7dc7 100755
--- a/java/com/android/dialer/about/res/raw/third_party_licenses
+++ b/java/com/android/dialer/about/res/raw/third_party_licenses
@@ -6424,13 +6424,14 @@
 
                                  Apache License
                            Version 2.0, January 2004
-                        https://www.apache.org/licenses/
+                        http://www.apache.org/licenses/
 
    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 
    1. Definitions.
 
       "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
 
       "Licensor" shall mean the copyright owner or entity authorized by
       the copyright owner that is granting the License.
@@ -6509,6 +6510,7 @@
       granted to You under this License for that Work shall terminate
       as of the date such litigation is filed.
 
+   4. Redistribution. You may reproduce and distribute copies of the
       Work or Derivative Works thereof in any medium, with or without
       modifications, and in Source or Object form, provided that You
       meet the following conditions:
@@ -6526,6 +6528,7 @@
           the Derivative Works; and
 
       (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
           include a readable copy of the attribution notices contained
           within such NOTICE file, excluding those notices that do not
           pertain to any part of the Derivative Works, in at least one
@@ -6543,7 +6546,9 @@
 
       You may add Your own copyright statement to Your modifications and
       may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
       for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
       the conditions stated in this License.
 
    5. Submission of Contributions. Unless You explicitly state otherwise,
@@ -6594,13 +6599,24 @@
 
    END OF TERMS AND CONDITIONS
 
-   Copyright 2015-2017 gRPC authors.
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
 
    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
 
-       https://www.apache.org/licenses/LICENSE-2.0
+       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,
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java
index b96b127..5f4b62c 100644
--- a/java/com/android/dialer/app/DialtactsActivity.java
+++ b/java/com/android/dialer/app/DialtactsActivity.java
@@ -1163,7 +1163,7 @@
     LogUtil.i("DialtactsActivity.enterSearchUi", "smart dial: %b", smartDialSearch);
     if (mStateSaved || getFragmentManager().isDestroyed()) {
       // Weird race condition where fragment is doing work after the activity is destroyed
-      // due to talkback being on (b/10209937). Just return since we can't do any
+      // due to talkback being on (a bug). Just return since we can't do any
       // constructive here.
       LogUtil.i(
           "DialtactsActivity.enterSearchUi",
diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java
index 016bce3..5890298 100644
--- a/java/com/android/dialer/app/calllog/CallLogAdapter.java
+++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java
@@ -361,7 +361,7 @@
                 "mExpandCollapseListener.onClick",
                 "%s is temporarily unavailable, requesting capabilities",
                 LogUtil.sanitizePhoneNumber(viewHolder.number));
-            // Refresh the capabilities when temporarily unavailable, see go/ec-temp-unavailable.
+            // Refresh the capabilities when temporarily unavailable.
             // Similarly to when we request capabilities the first time, the 'Share and call' button
             // won't pop in with the new capabilities. Instead the row needs to be collapsed and
             // expanded again.
diff --git a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java
index 78ec7a6..c0d30f5 100644
--- a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java
+++ b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java
@@ -103,7 +103,7 @@
     ContentValues values = new ContentValues();
     values.put(Voicemails.DELETED, "1");
     context.getContentResolver().update(voicemailUri, values, null, null);
-    // TODO(b/35440541): check which source package is changed. Don't need
+    // TODO(a bug): check which source package is changed. Don't need
     // to upload changes on foreign voicemails, they will get a PROVIDER_CHANGED
     uploadVoicemailLocalChangesToServer(context);
   }
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
index d5dfb06..fa73bb2 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -536,8 +536,7 @@
           CallIntentBuilder.increaseLightbringerCallButtonAppearInCollapsedCallLogItemCount();
           primaryActionButtonView.setTag(IntentProvider.getDuoVideoIntentProvider(number));
         } else {
-          primaryActionButtonView.setTag(
-              IntentProvider.getReturnVideoCallIntentProvider(number, accountHandle));
+          primaryActionButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number));
         }
         primaryActionButtonView.setContentDescription(
             TextUtils.expandTemplate(
@@ -793,7 +792,7 @@
 
     if (show) {
       if (!isLoaded) {
-        // b/31268128 for some unidentified reason showActions() can be called before the item is
+        // a bug for some unidentified reason showActions() can be called before the item is
         // loaded, causing NPE on uninitialized fields. Just log and return here, showActions() will
         // be called again once the item is loaded.
         LogUtil.e(
diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
index fff68d4..8e09cf8 100644
--- a/java/com/android/dialer/app/calllog/MissedCallNotifier.java
+++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
@@ -470,7 +470,7 @@
   private PendingIntent createCallLogPendingIntent(@Nullable Uri callUri) {
     Intent contentIntent =
         DialtactsActivity.getShowTabIntent(context, DialtactsPagerAdapter.TAB_INDEX_HISTORY);
-    // TODO (b/35486204): scroll to call
+    // TODO (a bug): scroll to call
     contentIntent.setData(callUri);
     return PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
   }
diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java b/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java
index 4fc956f..f9cb4bf 100644
--- a/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java
+++ b/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java
@@ -274,7 +274,7 @@
       @NonNull Context context, @Nullable NewCall voicemail) {
     Intent intent =
         DialtactsActivity.getShowTabIntent(context, DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL);
-    // TODO (b/35486204): scroll to this voicemail
+    // TODO (a bug): scroll to this voicemail
     if (voicemail != null) {
       intent.setData(voicemail.voicemailUri);
     }
diff --git a/java/com/android/dialer/app/list/RemoveView.java b/java/com/android/dialer/app/list/RemoveView.java
index 1d566c5..244f2da 100644
--- a/java/com/android/dialer/app/list/RemoveView.java
+++ b/java/com/android/dialer/app/list/RemoveView.java
@@ -68,7 +68,7 @@
     switch (action) {
       case DragEvent.ACTION_DRAG_ENTERED:
         // TODO: This is temporary solution and should be removed once accessibility for
-        // drag and drop is supported by framework(b/26871588).
+        // drag and drop is supported by framework(a bug).
         sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT);
         setAppearanceHighlighted();
         break;
diff --git a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml
index 0729d72..9e08f5f 100644
--- a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml
+++ b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml
@@ -27,6 +27,11 @@
       android:name="com.android.dialer.app.settings.DialerSettingsActivity"
       android:parentActivityName="com.android.dialer.app.DialtactsActivity"
       android:theme="@style/SettingsStyle">
+      <intent-filter>
+        <action android:name="android.intent.action.VIEW" />
+        <category android:name="android.intent.category.DEFAULT"/>
+        <data android:scheme="header"/>
+      </intent-filter>
     </activity>
 
     <!-- The entrance point for Phone UI.
@@ -41,7 +46,7 @@
       android:resizeableActivity="true"
       android:theme="@style/DialtactsActivityTheme"
       android:windowSoftInputMode="stateAlwaysHidden|adjustNothing">
-      <!-- LINT.IfChange -->
+
       <intent-filter>
         <action android:name="android.intent.action.DIAL"/>
 
@@ -107,7 +112,7 @@
         <category android:name="android.intent.category.DEFAULT"/>
         <category android:name="android.intent.category.TAB"/>
       </intent-filter>
-      <!-- LINT.ThenChange(//depot/google3/third_party/java_src/android_app/dialer/java/com/android/dialer/dialtacts/impl/AndroidManifest.xml) -->
+
 
       <meta-data
         android:name="com.android.keyguard.layout"
diff --git a/java/com/android/dialer/app/res/layout/call_log_list_item.xml b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
index 4382008..e0f9e63 100644
--- a/java/com/android/dialer/app/res/layout/call_log_list_item.xml
+++ b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
@@ -162,6 +162,8 @@
               android:layout_height="wrap_content"
               android:textColor="@color/call_log_voicemail_transcript_color"
               android:textSize="@dimen/call_log_voicemail_transcription_text_size"
+              android:focusable="true"
+              android:nextFocusDown="@+id/voicemail_transcription_branding"
               android:textIsSelectable="true"/>
 
             <TextView
@@ -170,6 +172,8 @@
               android:layout_height="wrap_content"
               android:textColor="@color/call_log_voicemail_transcript_branding_color"
               android:textSize="@dimen/call_log_voicemail_transcription_text_size"
+              android:focusable="true"
+              android:nextFocusUp="@id/voicemail_transcription"
               android:paddingTop="2dp"/>
 
           </LinearLayout>
diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
index 706f098..89c69ca 100644
--- a/java/com/android/dialer/app/settings/DialerSettingsActivity.java
+++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
@@ -18,6 +18,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.net.Uri;
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
@@ -51,11 +52,28 @@
 
   protected SharedPreferences mPreferences;
   private boolean migrationStatusOnBuildHeaders;
+  private List<Header> headers;
 
   @Override
   protected void onCreate(Bundle savedInstanceState) {
+    LogUtil.enterBlock("DialerSettingsActivity.onCreate");
     super.onCreate(savedInstanceState);
     mPreferences = PreferenceManager.getDefaultSharedPreferences(this.getApplicationContext());
+
+    Intent intent = getIntent();
+    Uri data = intent.getData();
+    if (data != null) {
+      String headerToOpen = data.getSchemeSpecificPart();
+      if (headerToOpen != null && headers != null) {
+        for (Header header : headers) {
+          if (headerToOpen.equals(header.fragment)) {
+            LogUtil.i("DialerSettingsActivity.onCreate", "switching to header: " + headerToOpen);
+            switchToHeader(header);
+            break;
+          }
+        }
+      }
+    }
   }
 
   @Override
@@ -72,6 +90,9 @@
 
   @Override
   public void onBuildHeaders(List<Header> target) {
+    // Keep a reference to the list of headers (since PreferenceActivity.getHeaders() is @Hide)
+    headers = target;
+
     if (showDisplayOptions()) {
       Header displayOptionsHeader = new Header();
       displayOptionsHeader.titleRes = R.string.display_options_title;
diff --git a/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java b/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java
index bc6ffb5..39ef3fa 100644
--- a/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java
+++ b/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java
@@ -92,7 +92,7 @@
       }
     }
     alertItem.updateStatus(statuses, this);
-    // TODO(twyen): b/30668323 support error from multiple sources.
+    // TODO(twyen): a bug support error from multiple sources.
     return;
   }
 
diff --git a/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml b/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml
index 65d0430..bb6c55f 100644
--- a/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml
+++ b/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml
@@ -1,5 +1,29 @@
+<!-- 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.
+-->
+
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.android.dialer.app.voicemail.error">
 
   <uses-permission android:name="android.permission.CALL_PHONE"/>
+
+  <application>
+    <receiver android:name=".PackageReplacedReceiver" android:exported="false">
+      <intent-filter>
+        <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
+      </intent-filter>
+    </receiver>
+  </application>
+
 </manifest>
diff --git a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
index 79e0383..9c8b146 100644
--- a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
+++ b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
@@ -129,7 +129,7 @@
           VoicemailErrorMessage.createRetryAction(context, status));
     }
 
-    // This should be an assertion error, but there's a bug in NYC-DR (b/31069259) that will
+    // This should be an assertion error, but there's a bug in NYC-DR (a bug) that will
     // sometimes give status mixed from multiple SIMs. There's no meaningful message to be displayed
     // from it, so just suppress the message.
     LogUtil.e("OmtpVoicemailMessageCreator.create", "Unhandled status: " + status);
diff --git a/java/com/android/dialer/app/voicemail/error/PackageReplacedReceiver.java b/java/com/android/dialer/app/voicemail/error/PackageReplacedReceiver.java
new file mode 100644
index 0000000..64d72b1
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/PackageReplacedReceiver.java
@@ -0,0 +1,109 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.provider.CallLog.Calls;
+import android.provider.VoicemailContract.Voicemails;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutor.Worker;
+import com.android.dialer.common.concurrent.DialerExecutorComponent;
+
+/** Receives MY_PACKAGE_REPLACED to check for legacy voicemail users. */
+public class PackageReplacedReceiver extends BroadcastReceiver {
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    LogUtil.enterBlock("PackageReplacedReceiver.onReceive");
+
+    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+    if (!prefs.contains(VoicemailTosMessageCreator.PREF_DIALER_FEATURE_VERSION_ACKNOWLEDGED_KEY)) {
+      setVoicemailFeatureVersionAsync(context);
+    }
+  }
+
+  private void setVoicemailFeatureVersionAsync(Context context) {
+    LogUtil.enterBlock("PackageReplacedReceiver.setVoicemailFeatureVersionAsync");
+
+    // Check if user is already using voicemail (ie do they have any voicemails), and set the
+    // acknowledged feature value accordingly.
+    PendingResult pendingResult = goAsync();
+    DialerExecutorComponent.get(context)
+        .dialerExecutorFactory()
+        .createNonUiTaskBuilder(new ExistingVoicemailCheck(context))
+        .onSuccess(
+            output -> {
+              LogUtil.i("PackageReplacedReceiver.setVoicemailFeatureVersionAsync", "success");
+              pendingResult.finish();
+            })
+        .onFailure(
+            throwable -> {
+              LogUtil.i("PackageReplacedReceiver.setVoicemailFeatureVersionAsync", "failure");
+              pendingResult.finish();
+            })
+        .build()
+        .executeParallel(null);
+  }
+
+  private static class ExistingVoicemailCheck implements Worker<Void, Void> {
+    private static final String[] PROJECTION = new String[] {Voicemails._ID};
+
+    private final Context context;
+
+    ExistingVoicemailCheck(Context context) {
+      this.context = context;
+    }
+
+    @TargetApi(android.os.Build.VERSION_CODES.M) // used for try with resources
+    @Override
+    public Void doInBackground(Void arg) throws Throwable {
+      LogUtil.i("PackageReplacedReceiver.ExistingVoicemailCheck.doInBackground", "");
+
+      // Check the database for existing voicemails.
+      boolean hasVoicemails = false;
+      Uri uri = Voicemails.buildSourceUri(context.getPackageName());
+      String whereClause = Calls.TYPE + " = " + Calls.VOICEMAIL_TYPE;
+      try (Cursor cursor =
+          context.getContentResolver().query(uri, PROJECTION, whereClause, null, null)) {
+        if (cursor == null) {
+          LogUtil.e(
+              "PackageReplacedReceiver.ExistingVoicemailCheck.doInBackground",
+              "failed to check for existing voicemails");
+        } else if (cursor.moveToNext()) {
+          hasVoicemails = true;
+        }
+      }
+
+      LogUtil.i(
+          "PackageReplacedReceiver.ExistingVoicemailCheck.doInBackground",
+          "has voicemails: " + hasVoicemails);
+      int version = hasVoicemails ? VoicemailTosMessageCreator.LEGACY_VOICEMAIL_FEATURE_VERSION : 0;
+      PreferenceManager.getDefaultSharedPreferences(context)
+          .edit()
+          .putInt(VoicemailTosMessageCreator.PREF_DIALER_FEATURE_VERSION_ACKNOWLEDGED_KEY, version)
+          .apply();
+      return null;
+    }
+  }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java
index bbba7ac..b357f9a 100644
--- a/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java
@@ -82,7 +82,7 @@
 
     // If visual voicemail is enabled, the CONFIGURATION_STATE should be either OK, PIN_NOT_SET,
     // or other failure code. CONFIGURATION_STATE_NOT_CONFIGURED means that the client has been
-    // shut down improperly (b/32371710). The client should be reset or the VVM tab will be
+    // shut down improperly (a bug). The client should be reset or the VVM tab will be
     // missing.
     if (Status.CONFIGURATION_STATE_NOT_CONFIGURED == status.configurationState
         && visualVoicemailEnabled) {
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java
index 63ebd19..96850ad 100644
--- a/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java
@@ -22,6 +22,7 @@
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.net.Uri;
 import android.os.Build;
 import android.preference.PreferenceManager;
 import android.support.annotation.Nullable;
@@ -40,6 +41,7 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.telephony.TelephonyManagerCompat;
 import com.android.dialer.configprovider.ConfigProviderBindings;
+import com.android.dialer.constants.Constants;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
 import com.android.voicemail.VisualVoicemailTypeExtensions;
@@ -52,14 +54,21 @@
  * terms of service for Verizon and for other carriers.
  */
 public class VoicemailTosMessageCreator {
-  // Flag to check which version of the Verizon ToS that the user has accepted.
-  public static final String VVM3_TOS_VERSION_ACCEPTED_KEY = "vvm3_tos_version_accepted";
+  // Preference key to check which version of the Verizon ToS that the user has accepted.
+  static final String PREF_VVM3_TOS_VERSION_ACCEPTED_KEY = "vvm3_tos_version_accepted";
 
-  // Flag to check which version of the Google Dialer ToS that the user has accepted.
-  public static final String DIALER_TOS_VERSION_ACCEPTED_KEY = "dialer_tos_version_accepted";
+  // Preference key to check which version of the Google Dialer ToS that the user has accepted.
+  static final String PREF_DIALER_TOS_VERSION_ACCEPTED_KEY = "dialer_tos_version_accepted";
 
-  public static final int CURRENT_VVM3_TOS_VERSION = 2;
-  public static final int CURRENT_DIALER_TOS_VERSION = 1;
+  // Preference key to check which feature version the user has acknowledged
+  static final String PREF_DIALER_FEATURE_VERSION_ACKNOWLEDGED_KEY =
+      "dialer_feature_version_acknowledged";
+
+  static final int CURRENT_VVM3_TOS_VERSION = 2;
+  static final int CURRENT_DIALER_TOS_VERSION = 1;
+  static final int LEGACY_VOICEMAIL_FEATURE_VERSION = 1; // original visual voicemail
+  static final int TRANSCRIPTION_VOICEMAIL_FEATURE_VERSION = 2; // adds voicemail transcription
+  static final int CURRENT_VOICEMAIL_FEATURE_VERSION = TRANSCRIPTION_VOICEMAIL_FEATURE_VERSION;
 
   private static final String ISO639_SPANISH = "es";
 
@@ -81,25 +90,28 @@
 
   @Nullable
   VoicemailErrorMessage maybeCreateTosMessage() {
-    if (hasAcceptedTos()) {
+    if (!canShowTos()) {
+      return null;
+    } else if (shouldShowTos()) {
+      logTosCreatedImpression();
+      return getTosMessage();
+    } else if (shouldShowPromo()) {
+      return getPromoMessage();
+    } else {
       return null;
     }
+  }
 
-    if (!shouldShowTos()) {
-      return null;
-    }
-
-    logTosCreatedImpression();
-
+  private VoicemailErrorMessage getTosMessage() {
     return new VoicemailTosMessage(
-            getTosTitle(),
-            getTosMessage(),
+            getNewUserTosTitle(),
+            getNewUserTosMessageText(),
             new Action(
                 getDeclineText(),
                 new OnClickListener() {
                   @Override
                   public void onClick(View v) {
-                    LogUtil.i("VoicemailTosMessageCreator.maybeShowTosMessage", "decline clicked");
+                    LogUtil.i("VoicemailTosMessageCreator.getTosMessage", "decline clicked");
                     PhoneAccountHandle handle =
                         new PhoneAccountHandle(
                             ComponentName.unflattenFromString(status.phoneAccountComponentName),
@@ -113,8 +125,10 @@
                 new OnClickListener() {
                   @Override
                   public void onClick(View v) {
-                    LogUtil.i("VoicemailTosMessageCreator.maybeShowTosMessage", "accept clicked");
+                    LogUtil.i("VoicemailTosMessageCreator.getTosMessage", "accept clicked");
                     recordTosAcceptance();
+                    // Accepting the TOS also acknowledges the latest features
+                    recordFeatureAcknowledgement();
                     logTosAcceptedImpression();
                     statusReader.refresh();
                   }
@@ -124,15 +138,65 @@
         .setImageResourceId(R.drawable.voicemail_tos_image);
   }
 
-  private boolean shouldShowTos() {
+  private VoicemailErrorMessage getPromoMessage() {
+    return new VoicemailTosMessage(
+            getExistingUserTosTitle(),
+            getExistingUserTosMessageText(),
+            new Action(
+                context.getString(R.string.dialer_terms_and_conditions_existing_user_setings),
+                new OnClickListener() {
+                  @Override
+                  public void onClick(View v) {
+                    LogUtil.i("VoicemailTosMessageCreator.getPromoMessage", "open settings");
+                    Intent intent =
+                        new Intent(Intent.ACTION_VIEW)
+                            .setComponent(
+                                new ComponentName(context, Constants.get().getSettingsActivity()))
+                            .setData(
+                                Uri.fromParts(
+                                    "header",
+                                    VoicemailComponent.get(context)
+                                        .getVoicemailClient()
+                                        .getSettingsFragment(),
+                                    null));
+                    context.startActivity(intent);
+                  }
+                }),
+            new Action(
+                context.getString(R.string.dialer_terms_and_conditions_existing_user_ack),
+                new OnClickListener() {
+                  @Override
+                  public void onClick(View v) {
+                    LogUtil.i("VoicemailTosMessageCreator.getPromoMessage", "acknowledge clicked");
+                    // Feature acknowledgement also means accepting TOS
+                    recordTosAcceptance();
+                    recordFeatureAcknowledgement();
+                    statusReader.refresh();
+                  }
+                },
+                true /* raised */))
+        .setModal(true)
+        .setImageResourceId(R.drawable.voicemail_tos_image);
+  }
+
+  private boolean canShowTos() {
     if (!isValidVoicemailType(status.type)) {
-      LogUtil.i("VoicemailTosMessageCreator.shouldShowTos", "unsupported type: " + status.type);
+      LogUtil.i("VoicemailTosMessageCreator.canShowTos", "unsupported type: " + status.type);
       return false;
     }
 
     if (status.getPhoneAccountHandle() == null
         || status.getPhoneAccountHandle().getComponentName() == null) {
-      LogUtil.i("VoicemailTosMessageCreator.shouldShowTos", "invalid phone account");
+      LogUtil.i("VoicemailTosMessageCreator.canShowTos", "invalid phone account");
+      return false;
+    }
+
+    return true;
+  }
+
+  private boolean shouldShowTos() {
+    if (hasAcceptedTos()) {
+      LogUtil.i("VoicemailTosMessageCreator.shouldShowTos", "already accepted TOS");
       return false;
     }
 
@@ -141,7 +205,7 @@
       return true;
     }
 
-    if (isVoicemailTranscriptionEnabled()) {
+    if (isVoicemailTranscriptionEnabled() && !isLegacyVoicemailUser()) {
       LogUtil.i(
           "VoicemailTosMessageCreator.shouldShowTos", "showing TOS for Google transcription users");
       return true;
@@ -150,6 +214,23 @@
     return false;
   }
 
+  private boolean shouldShowPromo() {
+    if (hasAcknowledgedFeatures()) {
+      LogUtil.i(
+          "VoicemailTosMessageCreator.shouldShowPromo", "already acknowledeged latest features");
+      return false;
+    }
+
+    if (isVoicemailTranscriptionEnabled()) {
+      LogUtil.i(
+          "VoicemailTosMessageCreator.shouldShowPromo",
+          "showing promo for Google transcription users");
+      return true;
+    }
+
+    return false;
+  }
+
   private static boolean isValidVoicemailType(String type) {
     if (type == null) {
       return false;
@@ -181,6 +262,7 @@
         "showing decline ToS dialog, status=" + status);
     final TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
     AlertDialog.Builder builder = new AlertDialog.Builder(context);
+    builder.setTitle(R.string.terms_and_conditions_decline_dialog_title);
     builder.setMessage(getTosDeclinedDialogMessageId());
     builder.setPositiveButton(
         getTosDeclinedDialogDowngradeId(),
@@ -249,24 +331,49 @@
 
   private boolean hasAcceptedTos() {
     if (isVvm3()) {
-      return preferences.getInt(VVM3_TOS_VERSION_ACCEPTED_KEY, 0) >= CURRENT_VVM3_TOS_VERSION;
+      return preferences.getInt(PREF_VVM3_TOS_VERSION_ACCEPTED_KEY, 0) >= CURRENT_VVM3_TOS_VERSION;
     } else {
-      return preferences.getInt(DIALER_TOS_VERSION_ACCEPTED_KEY, 0) >= CURRENT_DIALER_TOS_VERSION;
+      return preferences.getInt(PREF_DIALER_TOS_VERSION_ACCEPTED_KEY, 0)
+          >= CURRENT_DIALER_TOS_VERSION;
     }
   }
 
   private void recordTosAcceptance() {
     if (isVvm3()) {
-      preferences.edit().putInt(VVM3_TOS_VERSION_ACCEPTED_KEY, CURRENT_VVM3_TOS_VERSION).apply();
+      preferences
+          .edit()
+          .putInt(PREF_VVM3_TOS_VERSION_ACCEPTED_KEY, CURRENT_VVM3_TOS_VERSION)
+          .apply();
     } else {
       preferences
           .edit()
-          .putInt(DIALER_TOS_VERSION_ACCEPTED_KEY, CURRENT_DIALER_TOS_VERSION)
+          .putInt(PREF_DIALER_TOS_VERSION_ACCEPTED_KEY, CURRENT_DIALER_TOS_VERSION)
           .apply();
     }
     VoicemailComponent.get(context).getVoicemailClient().onTosAccepted(context);
   }
 
+  private boolean hasAcknowledgedFeatures() {
+    if (isVvm3()) {
+      return true;
+    }
+
+    return preferences.getInt(PREF_DIALER_FEATURE_VERSION_ACKNOWLEDGED_KEY, 0)
+        >= CURRENT_VOICEMAIL_FEATURE_VERSION;
+  }
+
+  private void recordFeatureAcknowledgement() {
+    preferences
+        .edit()
+        .putInt(PREF_DIALER_FEATURE_VERSION_ACKNOWLEDGED_KEY, CURRENT_VOICEMAIL_FEATURE_VERSION)
+        .apply();
+  }
+
+  private boolean isLegacyVoicemailUser() {
+    return preferences.getInt(PREF_DIALER_FEATURE_VERSION_ACKNOWLEDGED_KEY, 0)
+        == LEGACY_VOICEMAIL_FEATURE_VERSION;
+  }
+
   private void logTosCreatedImpression() {
     if (isVvm3()) {
       Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_V2_CREATED);
@@ -299,17 +406,30 @@
         : context.getString(R.string.verizon_terms_and_conditions_1_1_english, policyUrl);
   }
 
-  private CharSequence getDialerTos() {
+  private CharSequence getVvmDialerTos() {
     if (!isVoicemailTranscriptionEnabled()) {
       return "";
     }
 
-    if (isVvm3()) {
-      return context.getString(R.string.dialer_terms_and_conditions_for_verizon_1_0);
-    } else {
-      String learnMoreText = context.getString(R.string.dialer_terms_and_conditions_learn_more);
-      return context.getString(R.string.dialer_terms_and_conditions_1_0, learnMoreText);
+    return context.getString(R.string.dialer_terms_and_conditions_for_verizon_1_0);
+  }
+
+  private CharSequence getNewUserDialerTos() {
+    if (!isVoicemailTranscriptionEnabled()) {
+      return "";
     }
+
+    String learnMoreText = context.getString(R.string.dialer_terms_and_conditions_learn_more);
+    return context.getString(R.string.dialer_terms_and_conditions_1_0, learnMoreText);
+  }
+
+  private CharSequence getExistingUserDialerTos() {
+    if (!isVoicemailTranscriptionEnabled()) {
+      return "";
+    }
+
+    String learnMoreText = context.getString(R.string.dialer_terms_and_conditions_learn_more);
+    return context.getString(R.string.dialer_terms_and_conditions_existing_user, learnMoreText);
   }
 
   private CharSequence getAcceptText() {
@@ -336,13 +456,19 @@
     }
   }
 
-  private CharSequence getTosTitle() {
+  private CharSequence getNewUserTosTitle() {
     return isVvm3()
         ? context.getString(R.string.verizon_terms_and_conditions_title)
         : context.getString(R.string.dialer_terms_and_conditions_title);
   }
 
-  private CharSequence getTosMessage() {
+  private CharSequence getExistingUserTosTitle() {
+    return isVvm3()
+        ? context.getString(R.string.verizon_terms_and_conditions_title)
+        : context.getString(R.string.dialer_terms_and_conditions_existing_user_title);
+  }
+
+  private CharSequence getNewUserTosMessageText() {
     SpannableString spannableTos;
     if (isVvm3()) {
       // For verizon the TOS consist of three pieces: google dialer TOS, Verizon TOS message and
@@ -350,7 +476,7 @@
       CharSequence vvm3Details = getVvm3Tos();
       CharSequence tos =
           context.getString(
-              R.string.verizon_terms_and_conditions_message, getDialerTos(), vvm3Details);
+              R.string.verizon_terms_and_conditions_message, getVvmDialerTos(), vvm3Details);
       spannableTos = new SpannableString(tos);
       // Set the text style for the details part of the TOS
       int start = spannableTos.length() - vvm3Details.length();
@@ -365,7 +491,7 @@
     } else {
       // The TOS for everyone else, there are no details, but change to center alignment.
       CharSequence tos =
-          context.getString(R.string.dialer_terms_and_conditions_message, getDialerTos());
+          context.getString(R.string.dialer_terms_and_conditions_message, getNewUserDialerTos());
       spannableTos = new SpannableString(tos);
       spannableTos.setSpan(
           new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
@@ -375,11 +501,27 @@
 
       // Add 'Learn more' link for dialer TOS
       String learnMore = context.getString(R.string.dialer_terms_and_conditions_learn_more);
-      String linkUrl = context.getString(R.string.dialer_terms_and_conditions_learn_more_url);
-      return addLink(spannableTos, learnMore, linkUrl);
+      return addLink(spannableTos, learnMore, getLearnMoreUrl());
     }
   }
 
+  private CharSequence getExistingUserTosMessageText() {
+    SpannableString spannableTos;
+    // Change to center alignment.
+    CharSequence tos =
+        context.getString(R.string.dialer_terms_and_conditions_message, getExistingUserDialerTos());
+    spannableTos = new SpannableString(tos);
+    spannableTos.setSpan(
+        new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
+        0,
+        tos.length(),
+        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+    // Add 'Learn more' link for dialer TOS
+    String learnMore = context.getString(R.string.dialer_terms_and_conditions_learn_more);
+    return addLink(spannableTos, learnMore, getLearnMoreUrl());
+  }
+
   private SpannableString addLink(SpannableString spannable, String linkText, String linkUrl) {
     if (TextUtils.isEmpty(linkUrl) || TextUtils.isEmpty(linkText)) {
       return spannable;
@@ -398,6 +540,13 @@
     return spannable;
   }
 
+  private String getLearnMoreUrl() {
+    return ConfigProviderBindings.get(context)
+        .getString(
+            "voicemail_transcription_learn_more_url",
+            context.getString(R.string.dialer_terms_and_conditions_learn_more_url));
+  }
+
   private int getTosDeclinedDialogMessageId() {
     return isVvm3()
         ? R.string.verizon_terms_and_conditions_decline_dialog_message
diff --git a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
index 8e8106b..748b814 100644
--- a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
+++ b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
@@ -39,7 +39,7 @@
 public class Vvm3VoicemailMessageCreator {
 
   // Copied from com.android.phone.vvm.omtp.protocol.Vvm3EventHandler
-  // TODO(b/28380841): unbundle VVM client so we can access these values directly
+  // TODO(a bug): unbundle VVM client so we can access these values directly
   public static final int VMS_DNS_FAILURE = -9001;
   public static final int VMG_DNS_FAILURE = -9002;
   public static final int SPG_DNS_FAILURE = -9003;
diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
index 184a81f..4e143a5 100644
--- a/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
+++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
@@ -73,6 +73,7 @@
     android:paddingEnd="16dp"
     android:paddingTop="10dp"
     android:paddingBottom="10dp"
+    android:background="#ffffffff"
     android:orientation="horizontal">
     <TextView
       android:id="@+id/voicemail_tos_button_decline"
@@ -82,7 +83,7 @@
       android:layout_height="wrap_content"
       android:text="@string/verizon_terms_and_conditions_decline_english"/>
     <android.support.v4.widget.Space
-      android:layout_width="0dp"
+      android:layout_width="8dp"
       android:layout_height="match_parent"
       android:layout_weight="1"/>
     <TextView
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/strings.xml b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
index eb4877a..05082d8 100644
--- a/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
+++ b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
@@ -140,6 +140,8 @@
   <string name="verizon_terms_and_conditions_message"><xliff:g>%1$s</xliff:g> By turning on visual voicemail you agree to the Verizon Wireless terms and conditions:\n\n<xliff:g>%2$s</xliff:g></string>
 
   <string name="dialer_terms_and_conditions_title">Turn on visual voicemail</string>
+
+  <string name="dialer_terms_and_conditions_existing_user_title">New! Read your voicemail</string>
   <string name="dialer_terms_and_conditions_message"><xliff:g>%s</xliff:g></string>
 
   <string translatable="false" name="verizon_terms_and_conditions_1.1_english">
@@ -173,14 +175,16 @@
   See and listen to your messages, without having to call voicemail. Transcripts of your voicemail are provided by Google’s free transcription service. <xliff:g>%s</xliff:g>
   </string>
 
+  <string name="dialer_terms_and_conditions_existing_user">
+  Transcripts of your voicemail are now provided by Google’s free transcription service. %s
+  </string>
+
   <string name="dialer_terms_and_conditions_for_verizon_1.0">
   See and listen to your messages, without having to call voicemail.
   </string>
 
   <string name="dialer_terms_and_conditions_learn_more">Learn&#160;more</string>
-
-  <!-- TODO(mdooley): STOP SHIP, get real url, b/65734734 -->
-  <string translatable="false" name="dialer_terms_and_conditions_learn_more_url">https://www.google.com</string>
+  <string translatable="false" name="dialer_terms_and_conditions_learn_more_url">https://support.google.com/phoneapp/answer/2811844?hl=en%26ref_topic=7539039</string>
 
   <string translatable="false" name="verizon_terms_and_conditions_policy_url">http://www.verizon.com/about/privacy/policy/</string>
 
@@ -194,11 +198,16 @@
   <string translatable="false" name="dialer_terms_and_conditions_decline_english">No Thanks</string>
   <string translatable="false" name="dialer_terms_and_conditions_decline_spanish">Rechazar</string>
 
-  <string name="verizon_terms_and_conditions_decline_dialog_message">Visual voicemail will be disabled if the terms and conditions are declined.</string>
-  <string name="verizon_terms_and_conditions_decline_dialog_downgrade">Disable visual voicemail</string>
+  <string name="dialer_terms_and_conditions_existing_user_ack">Ok, got it</string>
+  <string name="dialer_terms_and_conditions_existing_user_setings">Settings</string>
 
-  <string name="dialer_terms_and_conditions_decline_dialog_message">Visual voicemail will be disabled if the terms and conditions are declined.</string>
-  <string name="dialer_terms_and_conditions_decline_dialog_downgrade">Disable visual voicemail</string>
+  <string name="terms_and_conditions_decline_dialog_title">Disable visual voicemail?</string>
+
+  <string name="verizon_terms_and_conditions_decline_dialog_message">Visual voicemail will be disabled if the terms and conditions are declined.</string>
+  <string name="verizon_terms_and_conditions_decline_dialog_downgrade">Disable</string>
+
+  <string name="dialer_terms_and_conditions_decline_dialog_message">Visual voicemail will be disabled if you turn off visual voicemail.</string>
+  <string name="dialer_terms_and_conditions_decline_dialog_downgrade">Disable</string>
 
   <string name="verizon_terms_and_conditions_decline_set_pin_dialog_message">Voicemail will only be accessible by calling *86. Set a new voicemail PIN to proceed.</string>
   <string name="verizon_terms_and_conditions_decline_set_pin_dialog_set_pin">Set PIN</string>
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/styles.xml b/java/com/android/dialer/app/voicemail/error/res/values/styles.xml
index 938c77a..bf70240 100644
--- a/java/com/android/dialer/app/voicemail/error/res/values/styles.xml
+++ b/java/com/android/dialer/app/voicemail/error/res/values/styles.xml
@@ -20,32 +20,32 @@
     <item name="android:layout_width">wrap_content</item>
     <item name="android:layout_height">48dp</item>
     <item name="android:gravity">end|center_vertical</item>
-    <item name="android:paddingStart">8dp</item>
-    <item name="android:paddingEnd">8dp</item>
     <item name="android:layout_marginStart">8dp</item>
     <item name="android:layout_marginEnd">8dp</item>
+    <item name="android:padding">8dp</item>
     <item name="android:textColor">@color/dialer_theme_color</item>
     <item name="android:fontFamily">"sans-serif-medium"</item>
     <item name="android:focusable">true</item>
     <item name="android:singleLine">true</item>
     <item name="android:textAllCaps">true</item>
     <item name="android:textSize">14sp</item>
+    <item name="android:minHeight">48dp</item>
   </style>
 
   <style name="ErrorActionDeclineStyle">
     <item name="android:layout_width">wrap_content</item>
     <item name="android:layout_height">48dp</item>
     <item name="android:gravity">end|center_vertical</item>
-    <item name="android:paddingStart">8dp</item>
-    <item name="android:paddingEnd">8dp</item>
     <item name="android:layout_marginStart">8dp</item>
     <item name="android:layout_marginEnd">8dp</item>
-    <item name="android:textColor">#80000000</item>
+    <item name="android:padding">8dp</item>
+    <item name="android:textColor">#757575</item>
     <item name="android:fontFamily">"sans-serif-medium"</item>
     <item name="android:focusable">true</item>
     <item name="android:singleLine">true</item>
     <item name="android:textAllCaps">true</item>
     <item name="android:textSize">14sp</item>
+    <item name="android:minHeight">48dp</item>
   </style>
 
   <style name="RaisedErrorActionStyle" parent="Widget.AppCompat.Button.Colored">
diff --git a/java/com/android/dialer/app/widget/SearchEditTextLayout.java b/java/com/android/dialer/app/widget/SearchEditTextLayout.java
index e7a707f..ebd6208 100644
--- a/java/com/android/dialer/app/widget/SearchEditTextLayout.java
+++ b/java/com/android/dialer/app/widget/SearchEditTextLayout.java
@@ -51,7 +51,6 @@
   private View mCollapsedSearchBox;
   private View mVoiceSearchButtonView;
   private View mOverflowButtonView;
-  private View mBackButtonView;
   private View mClearButtonView;
 
   private ValueAnimator mAnimator;
@@ -95,11 +94,6 @@
     mCollapsedSearchBox = findViewById(R.id.search_box_start_search);
     mVoiceSearchButtonView = findViewById(R.id.voice_search_button);
     mOverflowButtonView = findViewById(R.id.dialtacts_options_menu_button);
-    mBackButtonView = findViewById(R.id.search_back_button);
-    mBackButtonView
-        .getResources()
-        .getDrawable(R.drawable.quantum_ic_arrow_back_vd_theme_24, null)
-        .setAutoMirrored(true);
     mClearButtonView = findViewById(R.id.search_close_button);
 
     // Convert a long click into a click to expand the search box. Touch events are also
diff --git a/java/com/android/dialer/assisteddialing/ConcreteCreator.java b/java/com/android/dialer/assisteddialing/ConcreteCreator.java
index 1790b8f..9dc197c 100644
--- a/java/com/android/dialer/assisteddialing/ConcreteCreator.java
+++ b/java/com/android/dialer/assisteddialing/ConcreteCreator.java
@@ -71,7 +71,7 @@
     }
 
     if (!PreferenceManager.getDefaultSharedPreferences(context)
-        .getBoolean(context.getString(R.string.assisted_dialing_setting_toggle_key), false)) {
+        .getBoolean(context.getString(R.string.assisted_dialing_setting_toggle_key), true)) {
       LogUtil.i("ConcreteCreator.createNewAssistedDialingMediator", "disabled by local setting");
 
       return new AssistedDialingMediatorStub();
diff --git a/java/com/android/dialer/assisteddialing/Constraints.java b/java/com/android/dialer/assisteddialing/Constraints.java
index c4b5278..3766a6d 100644
--- a/java/com/android/dialer/assisteddialing/Constraints.java
+++ b/java/com/android/dialer/assisteddialing/Constraints.java
@@ -25,6 +25,8 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.DialerImpression;
+import com.android.dialer.logging.Logger;
 import com.google.i18n.phonenumbers.NumberParseException;
 import com.google.i18n.phonenumbers.PhoneNumberUtil;
 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
@@ -216,6 +218,8 @@
       // framework
       return Optional.of(phoneNumberUtil.parseAndKeepRawInput(numberToParse, userHomeCountryCode));
     } catch (NumberParseException e) {
+      Logger.get(context)
+          .logImpression(DialerImpression.Type.ASSISTED_DIALING_CONSTRAINT_PARSING_FAILURE);
       LogUtil.i("Constraints.parsePhoneNumber", "could not parse the number");
       return Optional.empty();
     }
@@ -227,6 +231,8 @@
     if (parsedPhoneNumber.get().hasCountryCode()
         && parsedPhoneNumber.get().getCountryCodeSource()
             != CountryCodeSource.FROM_DEFAULT_COUNTRY) {
+      Logger.get(context)
+          .logImpression(DialerImpression.Type.ASSISTED_DIALING_CONSTRAINT_NUMBER_HAS_COUNTRY_CODE);
       LogUtil.i(
           "Constraints.isNotInternationalNumber", "phone number already provided the country code");
       return false;
@@ -244,6 +250,8 @@
 
     if (parsedPhoneNumber.get().hasExtension()
         && !TextUtils.isEmpty(parsedPhoneNumber.get().getExtension())) {
+      Logger.get(context)
+          .logImpression(DialerImpression.Type.ASSISTED_DIALING_CONSTRAINT_NUMBER_HAS_EXTENSION);
       LogUtil.i("Constraints.doesNotHaveExtension", "phone number has an extension");
       return false;
     }
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 8e3c62d..806edfc 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
@@ -18,9 +18,9 @@
     xmlns:android="http://schemas.android.com/apk/res/android">
 
     <SwitchPreference
-        android:defaultValue="false"
+        android:defaultValue="true"
         android:key="@string/assisted_dialing_setting_toggle_key"
         android:title="@string/assisted_dialing_setting_title"
         android:summary="@string/assisted_dialing_setting_summary" />
 
-</PreferenceScreen>
\ No newline at end of file
+</PreferenceScreen>
diff --git a/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java
index 62bb9f3..09fd5f0 100644
--- a/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java
+++ b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java
@@ -262,7 +262,7 @@
   }
 
   /*
-   * TODO(maxwelb): b/27779827, non-e164 numbers can be blocked in the new form of blocking. As a
+   * TODO(maxwelb): a bug, non-e164 numbers can be blocked in the new form of blocking. As a
    * temporary workaround, determine which column of the database to query based on whether the
    * number is e164 or not.
    */
diff --git a/java/com/android/dialer/buildtype/bugfood/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/bugfood/BuildTypeAccessorImpl.java
deleted file mode 100644
index 45d72e0..0000000
--- a/java/com/android/dialer/buildtype/bugfood/BuildTypeAccessorImpl.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.buildtype;
-
-import com.android.dialer.proguard.UsedByReflection;
-
-/** Gets the build type. */
-@UsedByReflection(value = "BuildType.java")
-public class BuildTypeAccessorImpl implements BuildTypeAccessor {
-
-  @Override
-  @BuildType.Type
-  public int getBuildType() {
-    return BuildType.BUGFOOD;
-  }
-}
diff --git a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
deleted file mode 100644
index e1f2cdc..0000000
--- a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.buildtype;
-
-import com.android.dialer.proguard.UsedByReflection;
-
-/** Gets the build type. */
-@UsedByReflection(value = "BuildType.java")
-public class BuildTypeAccessorImpl implements BuildTypeAccessor {
-
-  @Override
-  @BuildType.Type
-  public int getBuildType() {
-    return BuildType.DOGFOOD;
-  }
-}
diff --git a/java/com/android/dialer/buildtype/fishfood/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/fishfood/BuildTypeAccessorImpl.java
deleted file mode 100644
index e5ad901..0000000
--- a/java/com/android/dialer/buildtype/fishfood/BuildTypeAccessorImpl.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.buildtype;
-
-import com.android.dialer.proguard.UsedByReflection;
-
-/** Gets the build type. */
-@UsedByReflection(value = "BuildType.java")
-public class BuildTypeAccessorImpl implements BuildTypeAccessor {
-
-  @Override
-  @BuildType.Type
-  public int getBuildType() {
-    return BuildType.FISHFOOD;
-  }
-}
diff --git a/java/com/android/dialer/buildtype/test/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/test/BuildTypeAccessorImpl.java
deleted file mode 100644
index 80a1cb7..0000000
--- a/java/com/android/dialer/buildtype/test/BuildTypeAccessorImpl.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.buildtype;
-
-import com.android.dialer.proguard.UsedByReflection;
-
-/** Gets the build type. */
-@UsedByReflection(value = "BuildType.java")
-public class BuildTypeAccessorImpl implements BuildTypeAccessor {
-
-  @Override
-  @BuildType.Type
-  public int getBuildType() {
-    return BuildType.TEST;
-  }
-}
diff --git a/java/com/android/dialer/callcomposer/CallComposerActivity.java b/java/com/android/dialer/callcomposer/CallComposerActivity.java
index 83fe2d9..60826fd 100644
--- a/java/com/android/dialer/callcomposer/CallComposerActivity.java
+++ b/java/com/android/dialer/callcomposer/CallComposerActivity.java
@@ -242,7 +242,7 @@
   }
 
   private void onCopyAndResizeImageFailure(Throwable throwable) {
-    // TODO(b/34279096) - gracefully handle message failure
+    // TODO(a bug) - gracefully handle message failure
     LogUtil.e("CallComposerActivity.onCopyAndResizeImageFailure", "copy Failed", throwable);
   }
 
diff --git a/java/com/android/dialer/callcomposer/GalleryComposerFragment.java b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
index 01e0674..2e6a28c 100644
--- a/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
+++ b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
@@ -137,7 +137,7 @@
                 })
             .onFailure(
                 throwable -> {
-                  // TODO(b/34279096) - gracefully handle message failure
+                  // TODO(a bug) - gracefully handle message failure
                   LogUtil.e(
                       "GalleryComposerFragment.onFailure", "data preparation failed", throwable);
                 })
@@ -303,7 +303,7 @@
     if (url != null) {
       copyAndResizeImage.executeParallel(Uri.parse(url));
     } else {
-      // TODO(b/34279096) - gracefully handle message failure
+      // TODO(a bug) - gracefully handle message failure
     }
   }
 }
diff --git a/java/com/android/dialer/callcomposer/camera/CameraPreview.java b/java/com/android/dialer/callcomposer/camera/CameraPreview.java
index 6581ad6..eaea789 100644
--- a/java/com/android/dialer/callcomposer/camera/CameraPreview.java
+++ b/java/com/android/dialer/callcomposer/camera/CameraPreview.java
@@ -65,7 +65,7 @@
   }
 
   // Opening camera is very expensive. Most of the ANR reports seem to be related to the camera.
-  // So we delay until the camera is actually needed.  See b/23287938
+  // So we delay until the camera is actually needed.  See a bug
   private void maybeOpenCamera() {
     boolean visible = mHost.getView().getVisibility() == View.VISIBLE;
     if (mTabHasBeenShown && visible && PermissionsUtil.hasCameraPermissions(getContext())) {
diff --git a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
index a713c55..b98dce7 100644
--- a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
+++ b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
@@ -57,7 +57,7 @@
 
   private final ImageView multimediaImage;
 
-  // TODO(maxwelb): Display this when location is stored - b/36160042
+  // TODO(maxwelb): Display this when location is stored - a bug
   @SuppressWarnings("unused")
   private final TextView multimediaAttachmentsNumber;
 
diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java
index 39a8065..68298e3 100644
--- a/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java
+++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java
@@ -148,7 +148,7 @@
   @Nullable
   @Override
   public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
-    // Javadoc states values is not nullable, even though it is annotated as such (b/38123194)!
+    // Javadoc states values is not nullable, even though it is annotated as such (a bug)!
     Assert.checkArgument(values != null);
 
     SQLiteDatabase database = databaseHelper.getWritableDatabase();
@@ -228,7 +228,7 @@
       @Nullable ContentValues values,
       @Nullable String selection,
       @Nullable String[] selectionArgs) {
-    // Javadoc states values is not nullable, even though it is annotated as such (b/38123194)!
+    // Javadoc states values is not nullable, even though it is annotated as such (a bug)!
     Assert.checkArgument(values != null);
 
     SQLiteDatabase database = databaseHelper.getWritableDatabase();
diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
index 589fe63..a3180a0 100644
--- a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
+++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
@@ -44,6 +44,7 @@
           .append(AnnotatedCallLog.PHOTO_URI + " string, ")
           .append(AnnotatedCallLog.PHOTO_ID + " integer, ")
           .append(AnnotatedCallLog.LOOKUP_URI + " string, ")
+          .append(AnnotatedCallLog.DURATION + " integer, ")
           .append(AnnotatedCallLog.NUMBER_TYPE_LABEL + " string, ")
           .append(AnnotatedCallLog.IS_READ + " integer, ")
           .append(AnnotatedCallLog.NEW + " integer, ")
@@ -55,7 +56,9 @@
           .append(AnnotatedCallLog.FEATURES + " integer, ")
           .append(AnnotatedCallLog.IS_BUSINESS + " integer, ")
           .append(AnnotatedCallLog.IS_VOICEMAIL + " integer, ")
-          .append(AnnotatedCallLog.CALL_TYPE + " integer")
+          .append(AnnotatedCallLog.TRANSCRIPTION + " integer, ")
+          .append(AnnotatedCallLog.CALL_TYPE)
+          .append(" integer")
           .append(");")
           .toString();
 
diff --git a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
index e79ffd0..832a9c2 100644
--- a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
+++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
@@ -218,6 +218,20 @@
 
     /** The MIME type of a {@link android.content.ContentProvider#getType(Uri)} single entry. */
     public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/annotated_call_log";
+
+    /**
+     * See {@link android.provider.CallLog.Calls#DURATION}.
+     *
+     * <p>TYPE: INTEGER (int)
+     */
+    public static final String DURATION = "duration";
+
+    /**
+     * See {@link android.provider.CallLog.Calls#TRANSCRIPTION}.
+     *
+     * <p>TYPE: TEXT
+     */
+    public static final String TRANSCRIPTION = "transcription";
   }
 
   /**
diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
index 681a86d..5c73933 100644
--- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
@@ -216,6 +216,8 @@
                   Calls.CACHED_LOOKUP_URI,
                   Calls.CACHED_NUMBER_TYPE,
                   Calls.CACHED_NUMBER_LABEL,
+                  Calls.DURATION,
+                  Calls.TRANSCRIPTION,
                   Calls.IS_READ,
                   Calls.NEW,
                   Calls.GEOCODED_LOCATION,
@@ -252,6 +254,8 @@
         int cachedLookupUriColumn = cursor.getColumnIndexOrThrow(Calls.CACHED_LOOKUP_URI);
         int cachedNumberTypeColumn = cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_TYPE);
         int cachedNumberLabelColumn = cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_LABEL);
+        int durationsColumn = cursor.getColumnIndexOrThrow(Calls.DURATION);
+        int transcriptionColumn = cursor.getColumnIndexOrThrow(Calls.TRANSCRIPTION);
         int isReadColumn = cursor.getColumnIndexOrThrow(Calls.IS_READ);
         int newColumn = cursor.getColumnIndexOrThrow(Calls.NEW);
         int geocodedLocationColumn = cursor.getColumnIndexOrThrow(Calls.GEOCODED_LOCATION);
@@ -276,6 +280,8 @@
           String cachedLookupUri = cursor.getString(cachedLookupUriColumn);
           int cachedNumberType = cursor.getInt(cachedNumberTypeColumn);
           String cachedNumberLabel = cursor.getString(cachedNumberLabelColumn);
+          int duration = cursor.getInt(durationsColumn);
+          String transcription = cursor.getString(transcriptionColumn);
           int isRead = cursor.getInt(isReadColumn);
           int isNew = cursor.getInt(newColumn);
           String geocodedLocation = cursor.getString(geocodedLocationColumn);
@@ -321,6 +327,8 @@
           populatePhoneAccountLabelAndColor(
               appContext, contentValues, phoneAccountComponentName, phoneAccountId);
           contentValues.put(AnnotatedCallLog.FEATURES, features);
+          contentValues.put(AnnotatedCallLog.DURATION, duration);
+          contentValues.put(AnnotatedCallLog.TRANSCRIPTION, transcription);
 
           if (existingAnnotatedCallLogIds.contains(id)) {
             mutations.update(id, contentValues);
diff --git a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java
index fbc7899..cadce4d 100644
--- a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java
+++ b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java
@@ -51,7 +51,7 @@
 
   private static final String SECRET_CODE_ACTION = "android.provider.Telephony.SECRET_CODE";
 
-  // TODO(erfanian): b/63995261 Replace with the platform/telecom constant when available.
+  // TODO(erfanian): a bug Replace with the platform/telecom constant when available.
   /**
    * Indicates that the call being placed originated from a known contact.
    *
@@ -59,7 +59,7 @@
    */
   public static final String ALLOW_ASSISTED_DIAL = "android.telecom.extra.ALLOW_ASSISTED_DIAL";
 
-  // TODO(erfanian): b/63995261 Replace with the platform/telecom constant when available.
+  // TODO(erfanian): a bug Replace with the platform/telecom constant when available.
   /**
    * Indicates that an outgoing call has undergone assisted dialing.
    *
@@ -68,7 +68,7 @@
    */
   public static final String IS_ASSISTED_DIALED = "android.telecom.extra.IS_ASSISTED_DIALED";
 
-  // TODO(erfanian): b/63995261 Replace with the platform/telecom API when available.
+  // TODO(erfanian): a bug Replace with the platform/telecom API when available.
   /** Additional information relating to the assisted dialing transformation. */
   public static final String ASSISTED_DIALING_EXTRAS =
       "android.telecom.extra.ASSISTED_DIALING_EXTRAS";
diff --git a/java/com/android/dialer/constants/Constants.java b/java/com/android/dialer/constants/Constants.java
index f9d07e3..a0c11f5 100644
--- a/java/com/android/dialer/constants/Constants.java
+++ b/java/com/android/dialer/constants/Constants.java
@@ -59,5 +59,8 @@
 
   public abstract String getUserAgent(Context context);
 
+  @NonNull
+  public abstract String getSettingsActivity();
+
   protected Constants() {}
 }
diff --git a/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java b/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java
index 38fd24b..c332e82 100644
--- a/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java
+++ b/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java
@@ -46,4 +46,10 @@
   public String getUserAgent(Context context) {
     return null;
   }
+
+  @NonNull
+  @Override
+  public String getSettingsActivity() {
+    return "com.android.dialer.app.settings.DialerSettingsActivity";
+  }
 }
diff --git a/java/com/android/dialer/constants/googledialer/ConstantsImpl.java b/java/com/android/dialer/constants/googledialer/ConstantsImpl.java
index e151344..003f748 100644
--- a/java/com/android/dialer/constants/googledialer/ConstantsImpl.java
+++ b/java/com/android/dialer/constants/googledialer/ConstantsImpl.java
@@ -58,4 +58,10 @@
 
     return userAgent.toString();
   }
+
+  @NonNull
+  @Override
+  public String getSettingsActivity() {
+    return "com.google.android.apps.dialer.settings.GoogleDialerSettingsActivity";
+  }
 }
diff --git a/java/com/android/dialer/contactphoto/ContactPhotoManagerImpl.java b/java/com/android/dialer/contactphoto/ContactPhotoManagerImpl.java
index 5dbdf5e..a225072 100644
--- a/java/com/android/dialer/contactphoto/ContactPhotoManagerImpl.java
+++ b/java/com/android/dialer/contactphoto/ContactPhotoManagerImpl.java
@@ -931,7 +931,7 @@
 
     /**
      * Maximum number of photos to preload. If the cache size is 2Mb and the expected average size
-     * of a photo is 4kb, then this number should be 2Mb/4kb = 500.
+     * of a photo is 4kb, then this number should be 2Ma bugkb = 500.
      */
     private static final int MAX_PHOTOS_TO_PRELOAD = 100;
 
diff --git a/java/com/android/dialer/database/DialerDatabaseHelper.java b/java/com/android/dialer/database/DialerDatabaseHelper.java
index 6dd7cf4..9a25812 100644
--- a/java/com/android/dialer/database/DialerDatabaseHelper.java
+++ b/java/com/android/dialer/database/DialerDatabaseHelper.java
@@ -1028,7 +1028,7 @@
      * Ignores contacts that have an unreasonably long lookup key. These are likely to be the result
      * of multiple (> 50) merged raw contacts, and are likely to cause OutOfMemoryExceptions within
      * SQLite, or cause memory allocation problems later on when iterating through the cursor set
-     * (see b/13133579)
+     * (see a bug)
      */
     String SELECT_IGNORE_LOOKUP_KEY_TOO_LONG_CLAUSE = "length(" + Phone.LOOKUP_KEY + ") < 1000";
 
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallManager.java b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
index 9f68978..681193c 100644
--- a/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
@@ -274,7 +274,7 @@
    *     when dialer is not in the foreground, and can not start {@link
    *     com.android.dialer.app.calllog.CallLogNotificationsService} to handle the event. The
    *     pendingResult allows dialer to hold on to resources when the event is handled in a
-   *     background thread. TODO(b/67015768): migrate CallLogNotificationsService to a
+   *     background thread. TODO(a bug): migrate CallLogNotificationsService to a
    *     JobIntentService so it can be used in the background.
    * @throws IllegalStateException if there's no session for the given id
    */
diff --git a/java/com/android/dialer/interactions/PhoneNumberInteraction.java b/java/com/android/dialer/interactions/PhoneNumberInteraction.java
index 9692dae..ac744cc 100644
--- a/java/com/android/dialer/interactions/PhoneNumberInteraction.java
+++ b/java/com/android/dialer/interactions/PhoneNumberInteraction.java
@@ -15,6 +15,7 @@
  */
 package com.android.dialer.interactions;
 
+import android.Manifest.permission;
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.AlertDialog;
@@ -227,15 +228,10 @@
     // It's possible for a shortcut to have been created, and then permissions revoked. To avoid a
     // crash when the user tries to use such a shortcut, check for this condition and ask the user
     // for the permission.
-    String[] deniedPhonePermissions =
-        PermissionsUtil.getPermissionsCurrentlyDenied(
-            mContext, PermissionsUtil.allPhoneGroupPermissionsUsedInDialer);
-    if (deniedPhonePermissions.length > 0) {
-      LogUtil.i(
-          "PhoneNumberInteraction.startInteraction",
-          "Need phone permissions: " + Arrays.toString(deniedPhonePermissions));
+    if (!PermissionsUtil.hasPhonePermissions(mContext)) {
+      LogUtil.i("PhoneNumberInteraction.startInteraction", "Need phone permissions: CALL_PHONE");
       ActivityCompat.requestPermissions(
-          (Activity) mContext, deniedPhonePermissions, REQUEST_CALL_PHONE);
+          (Activity) mContext, new String[] {permission.CALL_PHONE}, REQUEST_CALL_PHONE);
       return;
     }
 
diff --git a/java/com/android/dialer/logging/contact_source.proto b/java/com/android/dialer/logging/contact_source.proto
index 3a24da1..96ef9e1 100644
--- a/java/com/android/dialer/logging/contact_source.proto
+++ b/java/com/android/dialer/logging/contact_source.proto
@@ -15,7 +15,7 @@
   // time they made the spam report, which could be different from the
   // number's status at the time they made or received the call.
   // Type definitions are from the CachedContactInfo interface in
-  // google3/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java
+  // CachedNumberLookupService.java
   enum Type {
     UNKNOWN_SOURCE_TYPE = 0;
 
diff --git a/java/com/android/dialer/logging/dialer_impression.proto b/java/com/android/dialer/logging/dialer_impression.proto
index 0983918..cafcae3 100644
--- a/java/com/android/dialer/logging/dialer_impression.proto
+++ b/java/com/android/dialer/logging/dialer_impression.proto
@@ -13,7 +13,6 @@
   // It's perfectly acceptable for this enum to be large
   // Values should be from 1000 to 100000.
   enum Type {
-
     UNKNOWN_AOSP_EVENT_TYPE = 1000;
 
     // User opened the app
@@ -25,88 +24,94 @@
     // User pressed the speaker phone button again
     IN_CALL_SCREEN_TURN_ON_WIRED_OR_EARPIECE = 1003;
 
-    // Number not identified as spam and the user tapped the block/report spam button in the
-    // call log
+    // Number not identified as spam and the user tapped the block/report spam
+    // button in the call log
     CALL_LOG_BLOCK_REPORT_SPAM = 1004;
 
-    // Number identified as spam and the user tapped on the block number call log item
+    // Number identified as spam and the user tapped on the block number call
+    // log item
     CALL_LOG_BLOCK_NUMBER = 1005;
 
     // User tapped on the unblock number in the call log
-    // This does not deal with whether the user reported this spam or not while initially blocking
-    // For that refer to REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER. If the user had not reported it as
-    // spam they then have the option of directly unblocking the number, a success of which is
-    // logged in USER_ACTION_UNBLOCKED_NUMBER
+    // This does not deal with whether the user reported this spam or not while
+    // initially blocking For that refer to
+    // REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER. If the user had not reported it as
+    // spam they then have the option of directly unblocking the number, a
+    // success of which is logged in USER_ACTION_UNBLOCKED_NUMBER
     CALL_LOG_UNBLOCK_NUMBER = 1006;
 
     // Number was identified as spam, and the user tapped that it was not spam
     CALL_LOG_REPORT_AS_NOT_SPAM = 1007;
 
-    // Confirmation dialog in which the user confirmed that the number was not spam
+    // Confirmation dialog in which the user confirmed that the number was not
+    // spam
     DIALOG_ACTION_CONFIRM_NUMBER_NOT_SPAM = 1008;
 
     // User unblocked a number and also acknowledged that the number is not spam
-    // This happens when the user had initially blocked a number and also claimed the number was
-    // spam and had now proceeded to undo that.
+    // This happens when the user had initially blocked a number and also
+    // claimed the number was spam and had now proceeded to undo that.
     REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER = 1009
 
-      ;
+        ;
 
-    // A number that was identified as spam and the user proceeded to block it. However this
-    // impression was to make sure that while blocking the number the user also acknowledged that
-    // they were going to be reporting this as spam. There is no option for the user in this case
-    // to not report it as spam and block it only. The only flow is:
-    // system identified number as spam -> user wants to block it -> confirmation dialog shows up
-    // asking user to acknowledge they want to block and report as spam -> user acknowledges and
-    // this is when this impression is sent
+    // A number that was identified as spam and the user proceeded to block it.
+    // However this impression was to make sure that while blocking the number
+    // the user also acknowledged that they were going to be reporting this as
+    // spam. There is no option for the user in this case to not report it as
+    // spam and block it only. The only flow is: system identified number as
+    // spam -> user wants to block it -> confirmation dialog shows up asking
+    // user to acknowledge they want to block and report as spam -> user
+    // acknowledges and this is when this impression is sent
     DIALOG_ACTION_CONFIRM_NUMBER_SPAM_INDIRECTLY_VIA_BLOCK_NUMBER = 1010;
 
-    // User reported the number as spam by tick marking on report spam when blocking
-    // the number via call log. This is for case where the user wants to block a number and also
-    // report it as spam
-    REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG = 1011
+    // User reported the number as spam by tick marking on report spam when
+    // blocking the number via call log. This is for case where the user wants
+    // to block a number and also report it as spam
+    REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG =
+        1011
 
-      ;
+        ;
 
     // User made it to the last step and actually blocked the number
     USER_ACTION_BLOCKED_NUMBER = 1012
 
-      ;
+        ;
 
     // User made it to the last step and actually unblocked the number
     USER_ACTION_UNBLOCKED_NUMBER = 1013;
 
-    // User blocked a number, does not guarantee if the number was reported as spam or not
-    // To compute the number of blocked numbers that were reported as not spam and yet blocked
-    // Subtract this value from SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM. It would be
+    // User blocked a number, does not guarantee if the number was reported as
+    // spam or not To compute the number of blocked numbers that were reported
+    // as not spam and yet blocked Subtract this value from
+    // SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM. It would be
     // interesting to see how this value compares with
     // SPAM_AFTER_CALL_NOTIFICATION_REPORT_NUMBER_AS_NOT_SPAM
     SPAM_AFTER_CALL_NOTIFICATION_BLOCK_NUMBER = 1014;
 
-    // Displays the dialog for first time spam calls with actions "Not spam", "Block", and
-    // "Dismiss".
+    // Displays the dialog for first time spam calls with actions "Not spam",
+    // "Block", and "Dismiss".
     SPAM_AFTER_CALL_NOTIFICATION_SHOW_SPAM_DIALOG = 1015;
 
-    // Displays the dialog for the first time unknown calls with actions "Add contact",
-    // "Block/report spam", and "Dismiss".
+    // Displays the dialog for the first time unknown calls with actions "Add
+    // contact", "Block/report spam", and "Dismiss".
     SPAM_AFTER_CALL_NOTIFICATION_SHOW_NON_SPAM_DIALOG = 1016;
 
     // User added the number to contacts from the after call notification
     SPAM_AFTER_CALL_NOTIFICATION_ADD_TO_CONTACTS = 1019
 
-      ;
+        ;
 
     // User marked the number as spam on the after call notification flow
     SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM = 1020
 
-      ;
+        ;
 
     SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_NOT_SPAM_AND_BLOCKED = 1021;
 
     // User reported the number as not spam
     SPAM_AFTER_CALL_NOTIFICATION_REPORT_NUMBER_AS_NOT_SPAM = 1022
 
-      ;
+        ;
 
     // User dismissed the spam notification
     SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_SPAM_DIALOG = 1024;
@@ -114,21 +119,22 @@
     // User dismissed the non spam notification
     SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_NON_SPAM_DIALOG = 1025;
 
-    // From the service instead of an activity logs the number of times the number was marked as
-    // Spam by the user (e.g from the feedback prompt)
+    // From the service instead of an activity logs the number of times the
+    // number was marked as Spam by the user (e.g from the feedback prompt)
     SPAM_NOTIFICATION_SERVICE_ACTION_MARK_NUMBER_AS_SPAM = 1026;
 
-    // From the service instead of an activity logs the number of times the number was marked as
-    // Not Spam by the user (e.g from the feedback prompt)
+    // From the service instead of an activity logs the number of times the
+    // number was marked as Not Spam by the user (e.g from the feedback prompt)
     SPAM_NOTIFICATION_SERVICE_ACTION_MARK_NUMBER_AS_NOT_SPAM = 1027;
 
     // User is in a active call i.e either incoming or outgoing
-    // This is mainly so we can assign an impression event to a call event i.e so that we may be
-    // able to stitch different types of events if they make sense e.g user pressed a speaker button
-    // and we want to associate that to a call event
+    // This is mainly so we can assign an impression event to a call event i.e
+    // so that we may be able to stitch different types of events if they make
+    // sense e.g user pressed a speaker button and we want to associate that to
+    // a call event
     USER_PARTICIPATED_IN_A_CALL = 1028
 
-      ;
+        ;
 
     // Incoming call is a spam call
     INCOMING_SPAM_CALL = 1029;
@@ -155,60 +161,64 @@
     // User has clicked the change PIN action in the voicemail tab
     VOICEMAIL_ALERT_SET_PIN_CLICKED = 1046;
 
-    // User was not able to or did not participate in the call e.g missed calls, rejected calls
+    // User was not able to or did not participate in the call e.g missed calls,
+    // rejected calls
     USER_DID_NOT_PARTICIPATE_IN_CALL = 1047;
 
     // User deleted a call log entry
     USER_DELETED_CALL_LOG_ITEM = 1048
 
-      ;
+        ;
 
     // User tapped on "Send a message"
     CALL_LOG_SEND_MESSAGE = 1049
 
-      ;
+        ;
 
     // User tapped on "Add to contact"
     CALL_LOG_ADD_TO_CONTACT = 1050
 
-      ;
+        ;
 
     // User tapped on "Create new contact"
     CALL_LOG_CREATE_NEW_CONTACT = 1051
 
-      ;
+        ;
 
     // User deleted an entry from the voicemail tab
     VOICEMAIL_DELETE_ENTRY = 1052
 
-      ;
+        ;
 
-    // Voicemail call log entry was expanded. Could be either if the user tapped the voicemail
-    // call log entry or pressed the play button when the voicemail call log entry was not expanded
+    // Voicemail call log entry was expanded. Could be either if the user tapped
+    // the voicemail call log entry or pressed the play button when the
+    // voicemail call log entry was not expanded
     VOICEMAIL_EXPAND_ENTRY = 1053
 
-      ;
+        ;
 
-    // The play button for voicemail call log entry was tapped directly (i.e when the voicemail
-    // call log entry was not expanded and the playbutton was tapped)
-    VOICEMAIL_PLAY_AUDIO_DIRECTLY= 1054
+    // The play button for voicemail call log entry was tapped directly (i.e
+    // when the voicemail call log entry was not expanded and the playbutton was
+    // tapped)
+    VOICEMAIL_PLAY_AUDIO_DIRECTLY = 1054
 
-      ;
+        ;
 
     // The play button after expanding the voicemail call log entry was tapped
-    VOICEMAIL_PLAY_AUDIO_AFTER_EXPANDING_ENTRY= 1055
+    VOICEMAIL_PLAY_AUDIO_AFTER_EXPANDING_ENTRY = 1055
 
-      ;
+        ;
 
     // Incoming call was rejected from the notifications
-    REJECT_INCOMING_CALL_FROM_NOTIFICATION= 1056
+    REJECT_INCOMING_CALL_FROM_NOTIFICATION = 1056
 
-      ;
+        ;
 
-    // Incoming call was rejected from the answer screen including rejecting via sms and talkback
-    REJECT_INCOMING_CALL_FROM_ANSWER_SCREEN= 1057
+    // Incoming call was rejected from the answer screen including rejecting via
+    // sms and talkback
+    REJECT_INCOMING_CALL_FROM_ANSWER_SCREEN = 1057
 
-      ;
+        ;
 
     // User tapped block and spam buttons in context menu, same as buttons in
     // call log drop down
@@ -441,7 +451,7 @@
     MULTISELECT_DELETE_ENTRY_VIA_CONFIRMATION_DIALOG = 1209;
     MULTISELECT_CANCEL_CONFIRMATION_DIALOG_VIA_CANCEL_BUTTON = 1210;
     MULTISELECT_CANCEL_CONFIRMATION_DIALOG_VIA_CANCEL_TOUCH = 1211;
-    MULTISELECT_ROTATE_AND_SHOW_ACTION_MODE= 1212;
+    MULTISELECT_ROTATE_AND_SHOW_ACTION_MODE = 1212;
 
     // Impressions for verizon VVM with backup and transcription ToS
     VOICEMAIL_VVM3_TOS_V2_CREATED = 1213;
@@ -559,5 +569,17 @@
     CALL_DETAILS_IMS_VIDEO_CALL_BACK = 1284;
     CALL_DETAILS_LIGHTBRINGER_CALL_BACK = 1285;
     CALL_DETAILS_VOICE_CALL_BACK = 1286;
+
+    // Assisted Dialing related impressions tracking failures modes within the
+    // library.
+
+    // Indicates a failure to parse a provided number string in libphonenumber.
+    ASSISTED_DIALING_CONSTRAINT_PARSING_FAILURE = 1287;
+    // Indicates that the number attempting to be assisted dialed already
+    // specified a country code.
+    ASSISTED_DIALING_CONSTRAINT_NUMBER_HAS_COUNTRY_CODE = 1288;
+    // Indicates that the number attempting to be assisted dialed had an
+    // extension.
+    ASSISTED_DIALING_CONSTRAINT_NUMBER_HAS_EXTENSION = 1289;
   }
 }
diff --git a/java/com/android/dialer/main/impl/AndroidManifest.xml b/java/com/android/dialer/main/impl/AndroidManifest.xml
index 8edde50..6b7475f 100644
--- a/java/com/android/dialer/main/impl/AndroidManifest.xml
+++ b/java/com/android/dialer/main/impl/AndroidManifest.xml
@@ -32,7 +32,7 @@
       android:theme="@style/NuiMainActivityTheme"
       android:windowSoftInputMode="stateAlwaysHidden|adjustNothing">
 
-      <!-- LINT.IfChange -->
+
       <intent-filter>
         <action android:name="android.intent.action.DIAL"/>
 
@@ -98,7 +98,7 @@
         <category android:name="android.intent.category.DEFAULT"/>
         <category android:name="android.intent.category.TAB"/>
       </intent-filter>
-      <!-- LINT.ThenChange(//depot/google3/third_party/java_src/android_app/dialer/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml) -->
+
 
       <meta-data
         android:name="com.android.keyguard.layout"
diff --git a/java/com/android/dialer/notification/NotificationChannelManager.java b/java/com/android/dialer/notification/NotificationChannelManager.java
index 93caed5..790aac3 100644
--- a/java/com/android/dialer/notification/NotificationChannelManager.java
+++ b/java/com/android/dialer/notification/NotificationChannelManager.java
@@ -133,7 +133,7 @@
         new NotificationChannel(
             NotificationChannelId.ONGOING_CALL,
             context.getText(R.string.notification_channel_ongoing_call),
-            NotificationManager.IMPORTANCE_MAX);
+            NotificationManager.IMPORTANCE_DEFAULT);
     channel.setShowBadge(false);
     channel.enableLights(false);
     channel.enableVibration(false);
diff --git a/java/com/android/dialer/notification/VoicemailChannelUtils.java b/java/com/android/dialer/notification/VoicemailChannelUtils.java
index e2d0f3a..374619a 100644
--- a/java/com/android/dialer/notification/VoicemailChannelUtils.java
+++ b/java/com/android/dialer/notification/VoicemailChannelUtils.java
@@ -16,6 +16,7 @@
 
 package com.android.dialer.notification;
 
+import android.Manifest.permission;
 import android.annotation.TargetApi;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
@@ -25,6 +26,8 @@
 import android.provider.Settings;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.RequiresPermission;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.os.BuildCompat;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
@@ -34,6 +37,7 @@
 import android.util.ArraySet;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.PermissionsUtil;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -41,9 +45,10 @@
 /** Utilities for working with voicemail channels. */
 @TargetApi(VERSION_CODES.O)
 /* package */ final class VoicemailChannelUtils {
-  private static final String GLOBAL_VOICEMAIL_CHANNEL_ID = "phone_voicemail";
+  @VisibleForTesting static final String GLOBAL_VOICEMAIL_CHANNEL_ID = "phone_voicemail";
   private static final String PER_ACCOUNT_VOICEMAIL_CHANNEL_ID_PREFIX = "phone_voicemail_account_";
 
+  @SuppressWarnings("MissingPermission") // isSingleSimDevice() returns true if no permission
   static Set<String> getAllChannelIds(@NonNull Context context) {
     Assert.checkArgument(BuildCompat.isAtLeastO());
     Assert.isNotNull(context);
@@ -59,6 +64,7 @@
     return result;
   }
 
+  @SuppressWarnings("MissingPermission") // isSingleSimDevice() returns true if no permission
   static void createAllChannels(@NonNull Context context) {
     Assert.checkArgument(BuildCompat.isAtLeastO());
     Assert.isNotNull(context);
@@ -127,24 +133,38 @@
    */
   private static void createGlobalVoicemailChannel(@NonNull Context context) {
     NotificationChannel channel = newChannel(context, GLOBAL_VOICEMAIL_CHANNEL_ID, null);
+    migrateGlobalVoicemailSoundSettings(context, channel);
+    context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
+  }
 
+  @SuppressWarnings("MissingPermission") // checked with PermissionsUtil
+  private static void migrateGlobalVoicemailSoundSettings(
+      Context context, NotificationChannel channel) {
+    if (!PermissionsUtil.hasReadPhoneStatePermissions(context)) {
+      LogUtil.i(
+          "VoicemailChannelUtils.migrateGlobalVoicemailSoundSettings",
+          "missing phone permission, not migrating sound settings");
+      return;
+    }
     TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
     PhoneAccountHandle handle =
         telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
     if (handle == null) {
       LogUtil.i(
-          "VoicemailChannelUtils.createGlobalVoicemailChannel",
+          "VoicemailChannelUtils.migrateGlobalVoicemailSoundSettings",
           "phone account is null, not migrating sound settings");
-    } else if (!isChannelAllowedForAccount(context, handle)) {
-      LogUtil.i(
-          "VoicemailChannelUtils.createGlobalVoicemailChannel",
-          "phone account is not eligable, not migrating sound settings");
-    } else {
-      migrateVoicemailSoundSettings(context, channel, handle);
+      return;
     }
-    context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
+    if (!isChannelAllowedForAccount(context, handle)) {
+      LogUtil.i(
+          "VoicemailChannelUtils.migrateGlobalVoicemailSoundSettings",
+          "phone account is not eligable, not migrating sound settings");
+      return;
+    }
+    migrateVoicemailSoundSettings(context, channel, handle);
   }
 
+  @RequiresPermission(permission.READ_PHONE_STATE)
   private static List<PhoneAccountHandle> getAllEligableAccounts(@NonNull Context context) {
     List<PhoneAccountHandle> handles = new ArrayList<>();
     TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
@@ -210,6 +230,9 @@
   }
 
   private static boolean isSingleSimDevice(@NonNull Context context) {
+    if (!PermissionsUtil.hasReadPhoneStatePermissions(context)) {
+      return true;
+    }
     return context.getSystemService(TelephonyManager.class).getPhoneCount() <= 1;
   }
 
diff --git a/java/com/android/dialer/oem/CequintCallerIdManager.java b/java/com/android/dialer/oem/CequintCallerIdManager.java
index 7b6ddbc..df624e0 100644
--- a/java/com/android/dialer/oem/CequintCallerIdManager.java
+++ b/java/com/android/dialer/oem/CequintCallerIdManager.java
@@ -166,7 +166,7 @@
     Assert.isWorkerThread();
     Assert.isNotNull(number);
 
-    // Cequint is using custom arguments for content provider. See more details in b/35766080.
+    // Cequint is using custom arguments for content provider. See more details in a bug.
     try (Cursor cursor =
         context.getContentResolver().query(uri, EMPTY_PROJECTION, number, flags, null)) {
       if (cursor != null && cursor.moveToFirst()) {
diff --git a/java/com/android/dialer/oem/MotorolaUtils.java b/java/com/android/dialer/oem/MotorolaUtils.java
index 5f5bde6..a2757d3 100644
--- a/java/com/android/dialer/oem/MotorolaUtils.java
+++ b/java/com/android/dialer/oem/MotorolaUtils.java
@@ -102,7 +102,7 @@
    * @return true if the input is consumed and the intent is launched
    */
   public static boolean handleSpecialCharSequence(Context context, String input) {
-    // TODO(b/35395377): Add check for Motorola devices.
+    // TODO(a bug): Add check for Motorola devices.
     return MotorolaHiddenMenuKeySequence.handleCharSequence(context, input);
   }
 
diff --git a/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java
index f501792..8e08fb2 100644
--- a/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java
+++ b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java
@@ -159,7 +159,7 @@
         // ENTERPRISE_CONTENT_FILTER_URI in M doesn't support directory lookup
         uri = PhoneLookup.CONTENT_FILTER_URI;
       } else {
-        // b/25900607 in M. PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, encodes twice.
+        // a bug in M. PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, encodes twice.
         number = Uri.encode(number);
       }
     }
diff --git a/java/com/android/dialer/phonenumberproto/dialer_phone_number.proto b/java/com/android/dialer/phonenumberproto/dialer_phone_number.proto
index 97b7519..cd2ed50 100644
--- a/java/com/android/dialer/phonenumberproto/dialer_phone_number.proto
+++ b/java/com/android/dialer/phonenumberproto/dialer_phone_number.proto
@@ -97,8 +97,7 @@
   // leading zeros.
   //
   // Clients who use the parsing or conversion functionality of the i18n phone
-  // number libraries (go/phonenumbers) will have these fields set if necessary
-  // automatically.
+  // number libraries will have these fields set if necessary automatically.
   optional bool italian_leading_zero = 4;
   optional int32 number_of_leading_zeros = 8 [default = 1];
 
diff --git a/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java b/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java
index a53676b..40a3385 100644
--- a/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java
+++ b/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java
@@ -21,11 +21,13 @@
 import android.net.Uri;
 import android.os.Trace;
 import android.provider.CallLog;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
+import android.util.SparseIntArray;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.CompatUtils;
@@ -43,6 +45,71 @@
   private static final Set<String> LEGACY_UNKNOWN_NUMBERS =
       new HashSet<>(Arrays.asList("-1", "-2", "-3"));
 
+  /** The phone keypad letter mapping (see ITU E.161 or ISO/IEC 9995-8.) */
+  private static final SparseIntArray KEYPAD_MAP = new SparseIntArray();
+
+  static {
+    KEYPAD_MAP.put('a', '2');
+    KEYPAD_MAP.put('b', '2');
+    KEYPAD_MAP.put('c', '2');
+    KEYPAD_MAP.put('A', '2');
+    KEYPAD_MAP.put('B', '2');
+    KEYPAD_MAP.put('C', '2');
+
+    KEYPAD_MAP.put('d', '3');
+    KEYPAD_MAP.put('e', '3');
+    KEYPAD_MAP.put('f', '3');
+    KEYPAD_MAP.put('D', '3');
+    KEYPAD_MAP.put('E', '3');
+    KEYPAD_MAP.put('F', '3');
+
+    KEYPAD_MAP.put('g', '4');
+    KEYPAD_MAP.put('h', '4');
+    KEYPAD_MAP.put('i', '4');
+    KEYPAD_MAP.put('G', '4');
+    KEYPAD_MAP.put('H', '4');
+    KEYPAD_MAP.put('I', '4');
+
+    KEYPAD_MAP.put('j', '5');
+    KEYPAD_MAP.put('k', '5');
+    KEYPAD_MAP.put('l', '5');
+    KEYPAD_MAP.put('J', '5');
+    KEYPAD_MAP.put('K', '5');
+    KEYPAD_MAP.put('L', '5');
+
+    KEYPAD_MAP.put('m', '6');
+    KEYPAD_MAP.put('n', '6');
+    KEYPAD_MAP.put('o', '6');
+    KEYPAD_MAP.put('M', '6');
+    KEYPAD_MAP.put('N', '6');
+    KEYPAD_MAP.put('O', '6');
+
+    KEYPAD_MAP.put('p', '7');
+    KEYPAD_MAP.put('q', '7');
+    KEYPAD_MAP.put('r', '7');
+    KEYPAD_MAP.put('s', '7');
+    KEYPAD_MAP.put('P', '7');
+    KEYPAD_MAP.put('Q', '7');
+    KEYPAD_MAP.put('R', '7');
+    KEYPAD_MAP.put('S', '7');
+
+    KEYPAD_MAP.put('t', '8');
+    KEYPAD_MAP.put('u', '8');
+    KEYPAD_MAP.put('v', '8');
+    KEYPAD_MAP.put('T', '8');
+    KEYPAD_MAP.put('U', '8');
+    KEYPAD_MAP.put('V', '8');
+
+    KEYPAD_MAP.put('w', '9');
+    KEYPAD_MAP.put('x', '9');
+    KEYPAD_MAP.put('y', '9');
+    KEYPAD_MAP.put('z', '9');
+    KEYPAD_MAP.put('W', '9');
+    KEYPAD_MAP.put('X', '9');
+    KEYPAD_MAP.put('Y', '9');
+    KEYPAD_MAP.put('Z', '9');
+  }
+
   /** Returns true if it is possible to place a call to the given number. */
   public static boolean canPlaceCallsTo(CharSequence number, int presentation) {
     return presentation == CallLog.Calls.PRESENTATION_ALLOWED
@@ -60,29 +127,59 @@
    *   <li>their corresponding raw numbers are both global phone numbers (i.e., they can be accepted
    *       by {@link PhoneNumberUtils#isGlobalPhoneNumber(String)}) and {@link
    *       PhoneNumberUtils#compare(String, String)} deems them as "identical enough"; OR
-   *   <li>at least one of the raw numbers is not a global phone number and the two raw numbers are
-   *       exactly the same.
+   *   <li>neither of the raw numbers is a global phone number and they are identical.
    * </ul>
    *
-   * The raw number of a phone number is obtained by first translating any alphabetic letters
-   * ([A-Za-z]) into the equivalent numeric digits and then removing all separators. See {@link
-   * PhoneNumberUtils#convertKeypadLettersToDigits(String)} and {@link
-   * PhoneNumberUtils#stripSeparators(String)}.
+   * See {@link #convertAndStrip(String)} for how a raw number is obtained.
    */
   public static boolean compare(@Nullable String number1, @Nullable String number2) {
     if (number1 == null || number2 == null) {
       return Objects.equals(number1, number2);
     }
 
-    String rawNumber1 =
-        PhoneNumberUtils.stripSeparators(PhoneNumberUtils.convertKeypadLettersToDigits(number1));
-    String rawNumber2 =
-        PhoneNumberUtils.stripSeparators(PhoneNumberUtils.convertKeypadLettersToDigits(number2));
+    String rawNumber1 = convertAndStrip(number1);
+    String rawNumber2 = convertAndStrip(number2);
 
-    return PhoneNumberUtils.isGlobalPhoneNumber(rawNumber1)
-            && PhoneNumberUtils.isGlobalPhoneNumber(rawNumber2)
-        ? PhoneNumberUtils.compare(rawNumber1, rawNumber2)
-        : rawNumber1.equals(rawNumber2);
+    boolean isGlobalPhoneNumber1 = PhoneNumberUtils.isGlobalPhoneNumber(rawNumber1);
+    boolean isGlobalPhoneNumber2 = PhoneNumberUtils.isGlobalPhoneNumber(rawNumber2);
+
+    if (isGlobalPhoneNumber1 && isGlobalPhoneNumber2) {
+      return PhoneNumberUtils.compare(rawNumber1, rawNumber2);
+    }
+    if (!isGlobalPhoneNumber1 && !isGlobalPhoneNumber2) {
+      return rawNumber1.equals(rawNumber2);
+    }
+    return false;
+  }
+
+  /**
+   * Translating any alphabetic letters ([A-Za-z]) in the given phone number into the equivalent
+   * numeric digits and then removing all separators. The caller should ensure the number passed to
+   * this method is not null.
+   */
+  private static String convertAndStrip(@NonNull String number) {
+    int len = number.length();
+    if (len == 0) {
+      return number;
+    }
+
+    StringBuilder ret = new StringBuilder(len);
+    for (int i = 0; i < len; i++) {
+      char c = number.charAt(i);
+
+      // If the char isn't in KEYPAD_MAP, leave it alone for now.
+      c = (char) KEYPAD_MAP.get(c, c);
+
+      // Append the char to the result if it's a digit or non-separator.
+      int digit = Character.digit(c, 10);
+      if (digit != -1) {
+        ret.append(digit);
+      } else if (PhoneNumberUtils.isNonSeparator(c)) {
+        ret.append(c);
+      }
+    }
+
+    return ret.toString();
   }
 
   /**
diff --git a/java/com/android/dialer/proguard/proguard_base.flags b/java/com/android/dialer/proguard/proguard_base.flags
index 7b5794e..6d5d373 100644
--- a/java/com/android/dialer/proguard/proguard_base.flags
+++ b/java/com/android/dialer/proguard/proguard_base.flags
@@ -1,4 +1,3 @@
-# Copied from http://google3/java/com/google/android/apps/common/proguard/base.flags
 
 # This file is intended to contain proguard options that *nobody* would ever
 # not want, in *any* configuration - they ensure basic correctness, and have
diff --git a/java/com/android/dialer/proguard/proguard_release.flags b/java/com/android/dialer/proguard/proguard_release.flags
index 1429740..5c7aa83 100644
--- a/java/com/android/dialer/proguard/proguard_release.flags
+++ b/java/com/android/dialer/proguard/proguard_release.flags
@@ -1,4 +1,3 @@
-# Copied from http://google3/java/com/google/android/apps/common/proguard/release.flags
 
 # Used for building release binaries. Obfuscates, optimizes, and shrinks.
 
diff --git a/java/com/android/dialer/searchfragment/common/Projections.java b/java/com/android/dialer/searchfragment/common/Projections.java
index aaf9e80..63fac4c 100644
--- a/java/com/android/dialer/searchfragment/common/Projections.java
+++ b/java/com/android/dialer/searchfragment/common/Projections.java
@@ -42,7 +42,7 @@
   public static final int COMPANY_NAME = 12;
   public static final int NICKNAME = 13;
 
-  public static final String[] DATA_PROJECTION =
+  public static final String[] CP2_PROJECTION =
       new String[] {
         Data._ID, // 0
         Phone.TYPE, // 1
@@ -59,4 +59,20 @@
         Organization.COMPANY, // 12
         Nickname.NAME // 13
       };
+
+  public static final String[] DATA_PROJECTION =
+      new String[] {
+        Data._ID, // 0
+        Phone.TYPE, // 1
+        Phone.LABEL, // 2
+        Phone.NUMBER, // 3
+        Data.DISPLAY_NAME_PRIMARY, // 4
+        Data.PHOTO_ID, // 5
+        Data.PHOTO_THUMBNAIL_URI, // 6
+        Data.LOOKUP_KEY, // 7
+        Data.CARRIER_PRESENCE, // 8
+        Data.CONTACT_ID, // 9
+        Data.MIMETYPE, // 10
+        Data.SORT_KEY_PRIMARY, // 11
+      };
 }
diff --git a/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java b/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java
index 9a0ca00..84c22a2 100644
--- a/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java
+++ b/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java
@@ -31,7 +31,6 @@
 import android.support.annotation.Nullable;
 import android.support.v4.util.ArraySet;
 import android.text.TextUtils;
-import com.android.dialer.common.Assert;
 import com.android.dialer.searchfragment.common.Projections;
 import com.android.dialer.searchfragment.common.QueryFilteringUtil;
 import java.lang.annotation.Retention;
@@ -71,7 +70,7 @@
   }
 
   /**
-   * @param cursor with projection {@link Projections#DATA_PROJECTION}.
+   * @param cursor with projection {@link Projections#CP2_PROJECTION}.
    * @param query to filter cursor results.
    */
   ContactFilterCursor(Cursor cursor, @Nullable String query) {
@@ -123,8 +122,7 @@
     // Sort by display name, then build new cursor from coalesced contacts.
     // We sort the contacts so that they are displayed to the user in lexicographic order.
     Collections.sort(coalescedContacts, (o1, o2) -> o1.displayName().compareTo(o2.displayName()));
-    MatrixCursor newCursor =
-        new MatrixCursor(Projections.DATA_PROJECTION, coalescedContacts.size());
+    MatrixCursor newCursor = new MatrixCursor(Projections.CP2_PROJECTION, coalescedContacts.size());
     for (Cp2Contact contact : coalescedContacts) {
       newCursor.addRow(contact.toCursorRow());
     }
@@ -139,11 +137,13 @@
       if (contact.mimeType().equals(Phone.CONTENT_ITEM_TYPE)) {
         phoneContacts.add(contact);
       } else if (contact.mimeType().equals(Organization.CONTENT_ITEM_TYPE)) {
-        Assert.checkArgument(TextUtils.isEmpty(companyName));
-        companyName = contact.companyName();
+        // Since a contact can have more than one company name but they aren't visible to the user
+        // in our search UI, we can lazily concatenate them together to make them all searchable.
+        companyName += " " + contact.companyName();
       } else if (contact.mimeType().equals(Nickname.CONTENT_ITEM_TYPE)) {
-        Assert.checkArgument(TextUtils.isEmpty(nickName));
-        nickName = contact.nickName();
+        // Since a contact can have more than one nickname but they aren't visible to the user
+        // in our search UI, we can lazily concatenate them together to make them all searchable.
+        nickName += " " + contact.nickName();
       }
     }
 
diff --git a/java/com/android/dialer/searchfragment/cp2/Cp2Contact.java b/java/com/android/dialer/searchfragment/cp2/Cp2Contact.java
index f199f67..8e5e3e7 100644
--- a/java/com/android/dialer/searchfragment/cp2/Cp2Contact.java
+++ b/java/com/android/dialer/searchfragment/cp2/Cp2Contact.java
@@ -111,7 +111,7 @@
   }
 
   public Object[] toCursorRow() {
-    Object[] row = new Object[Projections.DATA_PROJECTION.length];
+    Object[] row = new Object[Projections.CP2_PROJECTION.length];
     row[Projections.ID] = phoneId();
     row[Projections.PHONE_TYPE] = phoneType();
     row[Projections.PHONE_LABEL] = phoneLabel();
diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java
index f1230c6..d3abbff 100644
--- a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java
+++ b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java
@@ -36,7 +36,7 @@
     super(
         context,
         Data.CONTENT_URI,
-        Projections.DATA_PROJECTION,
+        Projections.CP2_PROJECTION,
         whereStatement(),
         null,
         Phone.SORT_KEY_PRIMARY + " ASC");
diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
index 2544535..ef1b4fc 100644
--- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
+++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
@@ -412,7 +412,7 @@
 
   // Currently, setting up multiple FakeContentProviders doesn't work and results in this fragment
   // being untestable while it can query multiple datasources. This is a temporary fix.
-  // TODO(b/64099602): Remove this method and test this fragment with multiple data sources
+  // TODO(a bug): Remove this method and test this fragment with multiple data sources
   @VisibleForTesting
   public void setRemoteDirectoriesDisabled(boolean disabled) {
     remoteDirectoriesDisabledForTesting = disabled;
diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
index eb47273..64175be 100644
--- a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
+++ b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
@@ -44,7 +44,8 @@
       Uri.withAppendedPath(Phone.CONTENT_URI, "filter_enterprise");
 
   private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE = "length(" + Phone.NUMBER + ") < 1000";
-  private static final String MAX_RESULTS = "20";
+  private static final String PHONE_NUMBER_NOT_NULL = Phone.NUMBER + " IS NOT NULL";
+  private static final String MAX_RESULTS = "10";
 
   private final String query;
   private final List<Directory> directories;
@@ -55,7 +56,7 @@
         context,
         null,
         Projections.DATA_PROJECTION,
-        IGNORE_NUMBER_TOO_LONG_CLAUSE,
+        IGNORE_NUMBER_TOO_LONG_CLAUSE + " AND " + PHONE_NUMBER_NOT_NULL,
         null,
         Phone.SORT_KEY_PRIMARY);
     this.query = query;
diff --git a/java/com/android/dialer/searchfragment/testing/TestCursorSchema.java b/java/com/android/dialer/searchfragment/testing/TestCursorSchema.java
index 375fdb5..9117f72 100644
--- a/java/com/android/dialer/searchfragment/testing/TestCursorSchema.java
+++ b/java/com/android/dialer/searchfragment/testing/TestCursorSchema.java
@@ -23,7 +23,7 @@
 
   /**
    * If new rows are added to {@link
-   * com.android.dialer.searchfragment.common.Projections#DATA_PROJECTION}, this schema should be
+   * com.android.dialer.searchfragment.common.Projections#CP2_PROJECTION}, this schema should be
    * updated.
    */
   // TODO(67909522): remove these extra columns and remove all references to "Phone."
diff --git a/java/com/android/dialer/simulator/Simulator.java b/java/com/android/dialer/simulator/Simulator.java
index 4812fa5..bfa202c 100644
--- a/java/com/android/dialer/simulator/Simulator.java
+++ b/java/com/android/dialer/simulator/Simulator.java
@@ -31,7 +31,7 @@
   ActionProvider getActionProvider(Context context);
 
   /** The type of conference to emulate. */
-  // TODO(b/67785540): add VoLTE and CDMA conference call
+  // TODO(a bug): add VoLTE and CDMA conference call
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({
     CONFERENCE_TYPE_GSM,
diff --git a/java/com/android/dialer/strictmode/impl/SystemDialerStrictMode.java b/java/com/android/dialer/strictmode/impl/SystemDialerStrictMode.java
index b974ab1..09fdf5c 100644
--- a/java/com/android/dialer/strictmode/impl/SystemDialerStrictMode.java
+++ b/java/com/android/dialer/strictmode/impl/SystemDialerStrictMode.java
@@ -53,7 +53,7 @@
       // Because Android resets StrictMode policies after Application.onCreate is done, we set it
       // again right after.
       // See cl/105932355 for the discussion.
-      // See b/36951662 for the public bug.
+      // See a bug for the public bug.
       Handler handler = new Handler(Looper.myLooper());
       handler.postAtFrontOfQueue(() -> setRecommendedMainThreadPolicy(THREAD_DEATH_PENALTY));
     }
@@ -93,7 +93,7 @@
             .detectLeakedSqlLiteObjects();
     if (Build.VERSION.SDK_INT >= 26) {
       vmPolicyBuilder.detectContentUriWithoutPermission();
-      // TODO(azlatin): Enable detecting untagged sockets once: b/64840386 is fixed.
+      // TODO(azlatin): Enable detecting untagged sockets once: a bug is fixed.
       // vmPolicyBuilder.detectUntaggedSockets();
     }
     StrictMode.setVmPolicy(vmPolicyBuilder.build());
diff --git a/java/com/android/dialer/theme/res/values/colors.xml b/java/com/android/dialer/theme/res/values/colors.xml
index 3c8cabb..f44a7cc 100644
--- a/java/com/android/dialer/theme/res/values/colors.xml
+++ b/java/com/android/dialer/theme/res/values/colors.xml
@@ -38,6 +38,7 @@
 
   <!-- Primary text color in the Phone app -->
   <color name="dialer_primary_text_color">#333333</color>
+  <color name="dialer_primary_text_color_white">#ffffff</color>
   <color name="dialer_edit_text_hint_color">#DE78909C</color>
 
   <!-- Secondary text color in the Phone app -->
@@ -69,4 +70,7 @@
 
   <!-- Color of call type icons in call log, e.g. voicemail, video, WiFi, HD etc. -->
   <color name="call_type_icon_color">#89000000</color>
+
+  <!-- Color for bubble -->
+  <color name="dialer_end_call_button_color">#FFDF0000</color>
 </resources>
diff --git a/java/com/android/dialer/util/PermissionsUtil.java b/java/com/android/dialer/util/PermissionsUtil.java
index cb97368..02ea910 100644
--- a/java/com/android/dialer/util/PermissionsUtil.java
+++ b/java/com/android/dialer/util/PermissionsUtil.java
@@ -77,6 +77,10 @@
     return hasPermission(context, permission.CALL_PHONE);
   }
 
+  public static boolean hasReadPhoneStatePermissions(Context context) {
+    return hasPermission(context, permission.READ_PHONE_STATE);
+  }
+
   public static boolean hasContactsReadPermissions(Context context) {
     return hasPermission(context, permission.READ_CONTACTS);
   }
diff --git a/java/com/android/dialer/util/ViewUtil.java b/java/com/android/dialer/util/ViewUtil.java
index 81a32f9..211b3ed 100644
--- a/java/com/android/dialer/util/ViewUtil.java
+++ b/java/com/android/dialer/util/ViewUtil.java
@@ -19,16 +19,19 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.graphics.Paint;
+import android.graphics.Point;
 import android.os.PowerManager;
 import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.support.annotation.NonNull;
 import android.text.TextUtils;
 import android.util.TypedValue;
+import android.view.Display;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 import android.view.ViewTreeObserver.OnPreDrawListener;
+import android.view.WindowManager;
 import android.widget.TextView;
 import java.util.Locale;
 
@@ -139,4 +142,45 @@
     return Settings.Global.getFloat(contentResolver, Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0
         || powerManager.isPowerSaveMode();
   }
+
+  /**
+   * Get navigation bar height by calculating difference between app usable size and real screen
+   * size. Note that this won't work in multi-window mode so it's caller's responsibility to check
+   * if the app is in multi-window mode before using this.
+   *
+   * @param context Context
+   * @return Navigation bar height
+   */
+  public static int getNavigationBarHeight(Context context) {
+    WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+    Display display = windowManager.getDefaultDisplay();
+    Point appUsableSize = getAppUsableScreenSize(display);
+    Point realScreenSize = getRealScreenSize(display);
+
+    // Navigation bar on the right.
+    if (appUsableSize.x < realScreenSize.x) {
+      return appUsableSize.y;
+    }
+
+    // Navigation bar at the bottom.
+    if (appUsableSize.y < realScreenSize.y) {
+      return realScreenSize.y - appUsableSize.y;
+    }
+
+    // Navigation bar is not present.
+    return 0;
+  }
+
+  private static Point getAppUsableScreenSize(Display display) {
+    Point size = new Point();
+    display.getSize(size);
+    return size;
+  }
+
+  private static Point getRealScreenSize(Display display) {
+    Point size = new Point();
+    display.getRealSize(size);
+
+    return size;
+  }
 }
diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java
index d50be1a..ca0b5dc 100644
--- a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java
+++ b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java
@@ -21,22 +21,25 @@
 import android.view.View;
 import android.view.ViewGroup;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.time.Clock;
 
 /** {@link RecyclerView.Adapter} for the new voicemail call log fragment. */
 final class NewVoicemailAdapter extends RecyclerView.Adapter<NewVoicemailViewHolder> {
 
   private final Cursor cursor;
+  private final Clock clock;
 
   /** @param cursor whose projection is {@link VoicemailCursorLoader.VOICEMAIL_COLUMNS} */
-  NewVoicemailAdapter(Cursor cursor) {
+  NewVoicemailAdapter(Cursor cursor, Clock clock) {
     this.cursor = cursor;
+    this.clock = clock;
   }
 
   @Override
   public NewVoicemailViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
     LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
     View view = inflater.inflate(R.layout.new_voicemail_entry, viewGroup, false);
-    NewVoicemailViewHolder newVoicemailViewHolder = new NewVoicemailViewHolder(view);
+    NewVoicemailViewHolder newVoicemailViewHolder = new NewVoicemailViewHolder(view, clock);
     return newVoicemailViewHolder;
   }
 
diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java b/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java
index 1912e1e..9c1fd8b 100644
--- a/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java
+++ b/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java
@@ -54,7 +54,7 @@
   public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
     LogUtil.i("NewVoicemailFragment.onCreateLoader", "cursor size is %d", data.getCount());
     recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
-    recyclerView.setAdapter(new NewVoicemailAdapter(data));
+    recyclerView.setAdapter(new NewVoicemailAdapter(data, System::currentTimeMillis));
   }
 
   @Override
diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java b/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java
index daa24c8..8016563 100644
--- a/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java
+++ b/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java
@@ -19,11 +19,13 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
 import android.view.View;
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
 import com.android.dialer.contactphoto.ContactPhotoManager;
 import com.android.dialer.lettertile.LetterTileDrawable;
+import com.android.dialer.time.Clock;
 import com.android.dialer.voicemail.model.VoicemailEntry;
 
 /** {@link RecyclerView.ViewHolder} for the new voicemail tab. */
@@ -31,18 +33,37 @@
 
   private final Context context;
   private final TextView primaryTextView;
+  private final TextView secondaryTextView;
+  private final TextView transcriptionTextView;
   private final QuickContactBadge quickContactBadge;
+  private final Clock clock;
 
-  NewVoicemailViewHolder(View view) {
+  NewVoicemailViewHolder(View view, Clock clock) {
     super(view);
     this.context = view.getContext();
-    primaryTextView = (TextView) view.findViewById(R.id.primary_text);
+    primaryTextView = view.findViewById(R.id.primary_text);
+    secondaryTextView = view.findViewById(R.id.secondary_text);
+    transcriptionTextView = view.findViewById(R.id.transcription_text);
     quickContactBadge = view.findViewById(R.id.quick_contact_photo);
+    this.clock = clock;
   }
 
   void bind(Cursor cursor) {
     VoicemailEntry voicemailEntry = VoicemailCursorLoader.toVoicemailEntry(cursor);
     primaryTextView.setText(VoicemailEntryText.buildPrimaryVoicemailText(context, voicemailEntry));
+    secondaryTextView.setText(
+        VoicemailEntryText.buildSecondaryVoicemailText(context, clock, voicemailEntry));
+
+    String voicemailTranscription = voicemailEntry.transcription();
+
+    if (TextUtils.isEmpty(voicemailTranscription)) {
+      transcriptionTextView.setVisibility(View.GONE);
+      transcriptionTextView.setText(null);
+    } else {
+      transcriptionTextView.setVisibility(View.VISIBLE);
+      transcriptionTextView.setText(voicemailTranscription);
+    }
+
     setPhoto(voicemailEntry);
   }
 
diff --git a/java/com/android/dialer/voicemail/listui/VoicemailCursorLoader.java b/java/com/android/dialer/voicemail/listui/VoicemailCursorLoader.java
index 5a41765..e371e5e 100644
--- a/java/com/android/dialer/voicemail/listui/VoicemailCursorLoader.java
+++ b/java/com/android/dialer/voicemail/listui/VoicemailCursorLoader.java
@@ -39,8 +39,10 @@
         AnnotatedCallLog.PHOTO_URI,
         AnnotatedCallLog.PHOTO_ID,
         AnnotatedCallLog.LOOKUP_URI,
+        AnnotatedCallLog.DURATION,
         AnnotatedCallLog.GEOCODED_LOCATION,
-        AnnotatedCallLog.CALL_TYPE
+        AnnotatedCallLog.CALL_TYPE,
+        AnnotatedCallLog.TRANSCRIPTION
       };
 
   // Indexes for VOICEMAIL_COLUMNS
@@ -52,8 +54,10 @@
   private static final int PHOTO_URI = 5;
   private static final int PHOTO_ID = 6;
   private static final int LOOKUP_URI = 7;
-  private static final int GEOCODED_LOCATION = 8;
-  private static final int CALL_TYPE = 9;
+  private static final int DURATION = 8;
+  private static final int GEOCODED_LOCATION = 9;
+  private static final int CALL_TYPE = 10;
+  private static final int TRANSCRIPTION = 11;
 
   // TODO(zachh): Optimize indexes
   VoicemailCursorLoader(Context context) {
@@ -84,6 +88,8 @@
         .setPhotoUri(cursor.getString(PHOTO_URI))
         .setPhotoId(cursor.getLong(PHOTO_ID))
         .setLookupUri(cursor.getString(LOOKUP_URI))
+        .setDuration(cursor.getLong(DURATION))
+        .setTranscription(cursor.getString(TRANSCRIPTION))
         .setGeocodedLocation(cursor.getString(GEOCODED_LOCATION))
         .setCallType(cursor.getInt(CALL_TYPE))
         .build();
diff --git a/java/com/android/dialer/voicemail/listui/VoicemailEntryText.java b/java/com/android/dialer/voicemail/listui/VoicemailEntryText.java
index cf2fef2..f592201 100644
--- a/java/com/android/dialer/voicemail/listui/VoicemailEntryText.java
+++ b/java/com/android/dialer/voicemail/listui/VoicemailEntryText.java
@@ -18,7 +18,11 @@
 
 import android.content.Context;
 import android.text.TextUtils;
+import com.android.dialer.calllogutils.CallLogDates;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.time.Clock;
 import com.android.dialer.voicemail.model.VoicemailEntry;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Computes the primary text for voicemail entries.
@@ -39,4 +43,66 @@
     }
     return primaryText.toString();
   }
+
+  /**
+   * Uses the new date and location formatting rules to format the location and date in the new
+   * voicemail tab.
+   *
+   * <p>Rules: $Location • Date
+   *
+   * <p>Examples:
+   *
+   * <p>Jun 20 San Francisco • Now
+   *
+   * <p>Markham, ON • Jul 27
+   *
+   * <p>Toledo, OH • 12:15 PM
+   *
+   * <p>Date rules: if < 1 minute ago: "Now"; else if today: HH:MM(am|pm); else if < 3 days: day;
+   * else: MON D *
+   *
+   * @return $Location • Date
+   */
+  public static String buildSecondaryVoicemailText(
+      Context context, Clock clock, VoicemailEntry voicemailEntry) {
+    return secondaryTextPrefix(context, clock, voicemailEntry);
+  }
+
+  private static String secondaryTextPrefix(
+      Context context, Clock clock, VoicemailEntry voicemailEntry) {
+    StringBuilder secondaryText = new StringBuilder();
+    String location = voicemailEntry.geocodedLocation();
+    if (!TextUtils.isEmpty(location)) {
+      secondaryText.append(location);
+    }
+    if (secondaryText.length() > 0) {
+      secondaryText.append(" • ");
+    }
+    secondaryText.append(
+        CallLogDates.newCallLogTimestampLabel(
+            context, clock.currentTimeMillis(), voicemailEntry.timestamp()));
+
+    long duration = voicemailEntry.duration();
+    if (duration >= 0) {
+      secondaryText.append(" • ");
+      String formattedDuration = getVoicemailDuration(context, voicemailEntry);
+      secondaryText.append(formattedDuration);
+    }
+    return secondaryText.toString();
+  }
+
+  private static String getVoicemailDuration(Context context, VoicemailEntry voicemailEntry) {
+    long minutes = TimeUnit.SECONDS.toMinutes(voicemailEntry.duration());
+    long seconds = voicemailEntry.duration() - TimeUnit.MINUTES.toSeconds(minutes);
+
+    // The format for duration is "MM:SS" and we never expect the duration to be > 5 minutes
+    // However an incorrect duration could be set by the framework/someone to be >99, and in that
+    // case cap it at 99, for the UI to still be able to display it in "MM:SS" format.
+    if (minutes > 99) {
+      LogUtil.w(
+          "VoicemailEntryText.getVoicemailDuration", "Duration was %d", voicemailEntry.duration());
+      minutes = 99;
+    }
+    return context.getString(R.string.voicemailDurationFormat, minutes, seconds);
+  }
 }
diff --git a/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml b/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml
index bc15067..fe00992 100644
--- a/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml
+++ b/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml
@@ -72,6 +72,7 @@
           android:layout_marginStart="@dimen/call_log_entry_photo_text_margin"/>
     </LinearLayout>
 
+    <!-- TODO(uabdullah): Fix text cropping issue -->
     <TextView
         android:id="@+id/transcription_text"
         style="@style/SecondaryText"
diff --git a/java/com/android/dialer/voicemail/listui/res/values/strings.xml b/java/com/android/dialer/voicemail/listui/res/values/strings.xml
index 39c368a..a7df0ce 100644
--- a/java/com/android/dialer/voicemail/listui/res/values/strings.xml
+++ b/java/com/android/dialer/voicemail/listui/res/values/strings.xml
@@ -14,7 +14,10 @@
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License
  -->
-<resources>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
   <!-- String used to display voicemails from unknown numbers in the voicemail tab.  [CHAR LIMIT=30] -->
   <string name="voicemail_entry_unknown">Unknown</string>
+
+  <!-- Format for duration of voicemails which are displayed when viewing voicemail. For example "01:22" -->
+  <string name="voicemailDurationFormat"><xliff:g example="10" id="minutes">%1$02d</xliff:g>:<xliff:g example="20" id="seconds">%2$02d</xliff:g></string>
 </resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/voicemail/model/VoicemailEntry.java b/java/com/android/dialer/voicemail/model/VoicemailEntry.java
index 00e1757..fc548f1 100644
--- a/java/com/android/dialer/voicemail/model/VoicemailEntry.java
+++ b/java/com/android/dialer/voicemail/model/VoicemailEntry.java
@@ -31,6 +31,7 @@
         .setTimestamp(0)
         .setNumber(DialerPhoneNumber.getDefaultInstance())
         .setPhotoId(0)
+        .setDuration(0)
         .setCallType(0);
   }
 
@@ -58,6 +59,11 @@
   @Nullable
   public abstract String geocodedLocation();
 
+  public abstract long duration();
+
+  @Nullable
+  public abstract String transcription();
+
   public abstract int callType();
 
   /** Builder for {@link VoicemailEntry}. */
@@ -80,6 +86,10 @@
 
     public abstract Builder setLookupUri(@Nullable String lookupUri);
 
+    public abstract Builder setDuration(long duration);
+
+    public abstract Builder setTranscription(@Nullable String transcription);
+
     public abstract Builder setGeocodedLocation(@Nullable String geocodedLocation);
 
     public abstract Builder setCallType(int callType);
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
index aa17dc4..aa96c4f 100644
--- a/java/com/android/incallui/CallButtonPresenter.java
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -456,7 +456,7 @@
 
     final boolean hasCameraPermission =
         isVideo && VideoUtils.hasCameraPermissionAndShownPrivacyToast(mContext);
-    // Disabling local video doesn't seem to work when dialing. See b/30256571.
+    // Disabling local video doesn't seem to work when dialing. See a bug.
     final boolean showPauseVideo =
         isVideo
             && call.getState() != DialerCall.State.DIALING
@@ -500,7 +500,7 @@
    * @return {@code true} if downgrading to an audio-only call from a video call is supported.
    */
   private boolean isDowngradeToAudioSupported(DialerCall call) {
-    // TODO(b/33676907): If there is an RCS video share session, return true here
+    // TODO(a bug): If there is an RCS video share session, return true here
     return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO);
   }
 
diff --git a/java/com/android/incallui/CallerInfo.java b/java/com/android/incallui/CallerInfo.java
index 809ed59..4a9cf21 100644
--- a/java/com/android/incallui/CallerInfo.java
+++ b/java/com/android/incallui/CallerInfo.java
@@ -510,7 +510,7 @@
       Log.e(TAG, "Cannot access VoiceMail.", se);
     }
     // TODO: There is no voicemail picture?
-    // FIXME: FIND ANOTHER ICON
+
     // photoResource = android.R.drawable.badge_voicemail;
     return this;
   }
diff --git a/java/com/android/incallui/CallerInfoAsyncQuery.java b/java/com/android/incallui/CallerInfoAsyncQuery.java
index 8fc9c4f..f9d8da8 100644
--- a/java/com/android/incallui/CallerInfoAsyncQuery.java
+++ b/java/com/android/incallui/CallerInfoAsyncQuery.java
@@ -103,7 +103,7 @@
           public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
             Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onQueryComplete");
             // If there are no other directory queries, make sure that the listener is
-            // notified of this result.  see b/27621628
+            // notified of this result.  see a bug
             if ((ci != null && ci.contactExists)
                 || !startOtherDirectoriesQuery(token, context, info, listener, cookie)) {
               if (listener != null && ci != null) {
@@ -206,7 +206,7 @@
     // The current implementation of multiple async query runs in single handler thread
     // in AsyncQueryHandler.
     // intermediateListener.onQueryComplete is also called from the same caller thread.
-    // TODO(b/26019872): use thread pool instead of single thread.
+    // TODO(a bug): use thread pool instead of single thread.
     for (int i = 0; i < size; i++) {
       long directoryId = directoryIds[i];
       Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber, directoryId);
diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java
index 2e3d721..c3a68c0 100644
--- a/java/com/android/incallui/InCallActivity.java
+++ b/java/com/android/incallui/InCallActivity.java
@@ -516,6 +516,14 @@
     common.showInternationalCallOnWifiDialog(call);
   }
 
+  @Override
+  public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
+    super.onMultiWindowModeChanged(isInMultiWindowMode);
+    if (!isInMultiWindowMode) {
+      common.updateNavigationBar(isDialpadVisible());
+    }
+  }
+
   public void setAllowOrientationChange(boolean allowOrientationChange) {
     if (this.allowOrientationChange == allowOrientationChange) {
       return;
diff --git a/java/com/android/incallui/InCallActivityCommon.java b/java/com/android/incallui/InCallActivityCommon.java
index 0a7c268..d39ce22 100644
--- a/java/com/android/incallui/InCallActivityCommon.java
+++ b/java/com/android/incallui/InCallActivityCommon.java
@@ -305,6 +305,7 @@
       }
       showDialpadRequest = DIALPAD_REQUEST_NONE;
     }
+    updateNavigationBar(isDialpadVisible());
 
     if (showPostCharWaitDialogOnResume) {
       showPostCharWaitDialog(showPostCharWaitDialogCallId, showPostCharWaitDialogChars);
@@ -328,7 +329,7 @@
   public void onStop() {
     // Disconnects call waiting for account when activity is hidden e.g. user press home button.
     // This is necessary otherwise the pending call will stuck on account choose and no new call
-    // will be able to create. See b/63600434 for more details.
+    // will be able to create. See a bug for more details.
     // Skip this on locked screen since the activity may go over life cycle and start again.
     if (!isRecreating
         && !inCallActivity.getSystemService(KeyguardManager.class).isKeyguardLocked()) {
@@ -712,6 +713,16 @@
     dialog.show();
   }
 
+  void updateNavigationBar(boolean isDialpadVisible) {
+    if (!inCallActivity.isInMultiWindowMode()) {
+      View navigationBarBackground =
+          inCallActivity.getWindow().findViewById(R.id.navigation_bar_background);
+      if (navigationBarBackground != null) {
+        navigationBarBackground.setVisibility(isDialpadVisible ? View.VISIBLE : View.GONE);
+      }
+    }
+  }
+
   public boolean showDialpadFragment(boolean show, boolean animate) {
     // If the dialpad is already visible, don't animate in. If it's gone, don't animate out.
     boolean isDialpadVisible = isDialpadVisible();
@@ -772,6 +783,7 @@
     dialpadFragmentManager.executePendingTransactions();
 
     Logger.get(inCallActivity).logScreenView(ScreenEvent.Type.INCALL_DIALPAD, inCallActivity);
+    updateNavigationBar(true /* isDialpadVisible */);
   }
 
   private void performHideDialpadFragment() {
@@ -789,6 +801,7 @@
       transaction.commitAllowingStateLoss();
       fragmentManager.executePendingTransactions();
     }
+    updateNavigationBar(false /* isDialpadVisible */);
   }
 
   public boolean isDialpadVisible() {
diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java
index 1ba3b5d..b8a2baa 100644
--- a/java/com/android/incallui/InCallPresenter.java
+++ b/java/com/android/incallui/InCallPresenter.java
@@ -452,6 +452,7 @@
 
     if (inCallActivity != null) {
       if (mInCallActivity == null) {
+        mContext = inCallActivity.getApplicationContext();
         updateListeners = true;
         LogUtil.i("InCallPresenter.updateActivity", "UI Initialized");
       } else {
@@ -500,7 +501,7 @@
     //           (2) All calls could disconnect and then get a new incoming call before the
     //               activity is destroyed.
     //
-    // b/1122139 - We previously had a check for mServiceConnected here as well, but there are
+    // a bug - We previously had a check for mServiceConnected here as well, but there are
     // cases where we need to recalculate the current state even if the service in not
     // connected.  In particular the case where startOrFinish() is called while the app is
     // already finish()ing. In that case, we skip updating the state with the knowledge that
@@ -730,7 +731,7 @@
     // incall activity for that call will still exist (even if it's not visible). In the case of
     // an incoming call in that situation, just disconnect that "waiting for account" call and
     // dismiss the dialog. The same activity will be reused to handle the new incoming call. See
-    // b/33247755 for more details.
+    // a bug for more details.
     DialerCall waitingForAccountCall;
     if (newState == InCallState.INCOMING
         && (waitingForAccountCall = callList.getWaitingForAccountCall()) != null) {
diff --git a/java/com/android/incallui/InCallServiceImpl.java b/java/com/android/incallui/InCallServiceImpl.java
index 2c45cb3..402e002 100644
--- a/java/com/android/incallui/InCallServiceImpl.java
+++ b/java/com/android/incallui/InCallServiceImpl.java
@@ -38,6 +38,7 @@
 public class InCallServiceImpl extends InCallService {
 
   private ReturnToCallController returnToCallController;
+  private NewReturnToCallController newReturnToCallController;
 
   @Override
   public void onCallAudioStateChanged(CallAudioState audioState) {
@@ -97,6 +98,9 @@
     if (ReturnToCallController.isEnabled(this)) {
       returnToCallController = new ReturnToCallController(this);
     }
+    if (NewReturnToCallController.isEnabled(this)) {
+      newReturnToCallController = new NewReturnToCallController(this);
+    }
 
     IBinder iBinder = super.onBind(intent);
     Trace.endSection();
@@ -125,6 +129,10 @@
       returnToCallController.tearDown();
       returnToCallController = null;
     }
+    if (newReturnToCallController != null) {
+      newReturnToCallController.tearDown();
+      newReturnToCallController = null;
+    }
     Trace.endSection();
   }
 }
diff --git a/java/com/android/incallui/NewReturnToCallController.java b/java/com/android/incallui/NewReturnToCallController.java
new file mode 100644
index 0000000..cd69ea1
--- /dev/null
+++ b/java/com/android/incallui/NewReturnToCallController.java
@@ -0,0 +1,283 @@
+/*
+ * 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.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.CallAudioState;
+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.android.dialer.telecom.TelecomUtil;
+import com.android.incallui.InCallPresenter.InCallUiListener;
+import com.android.incallui.audiomode.AudioModeProvider;
+import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.CallList.Listener;
+import com.android.incallui.call.DialerCall;
+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.util.ArrayList;
+import java.util.List;
+
+/**
+ * Listens for events relevant to the return-to-call bubble and updates the bubble's state as
+ * necessary
+ */
+public class NewReturnToCallController implements InCallUiListener, Listener, AudioModeListener {
+
+  public static final String RETURN_TO_CALL_EXTRA_KEY = "RETURN_TO_CALL_BUBBLE";
+
+  private final Context context;
+
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  NewBubble bubble;
+
+  private CallAudioState audioState;
+
+  private final PendingIntent toggleSpeaker;
+  private final PendingIntent showSpeakerSelect;
+  private final PendingIntent toggleMute;
+  private final PendingIntent endCall;
+  private final PendingIntent fullScreen;
+
+  public static boolean isEnabled(Context context) {
+    return ConfigProviderBindings.get(context).getBoolean("enable_return_to_call_bubble_v2", false);
+  }
+
+  public NewReturnToCallController(Context context) {
+    this.context = context;
+
+    toggleSpeaker = createActionIntent(ReturnToCallActionReceiver.ACTION_TOGGLE_SPEAKER);
+    showSpeakerSelect =
+        createActionIntent(ReturnToCallActionReceiver.ACTION_SHOW_AUDIO_ROUTE_SELECTOR);
+    toggleMute = createActionIntent(ReturnToCallActionReceiver.ACTION_TOGGLE_MUTE);
+    endCall = createActionIntent(ReturnToCallActionReceiver.ACTION_END_CALL);
+
+    Intent activityIntent = InCallActivity.getIntent(context, false, false, false);
+    activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    activityIntent.putExtra(RETURN_TO_CALL_EXTRA_KEY, true);
+    fullScreen =
+        PendingIntent.getActivity(
+            context, InCallActivity.PENDING_INTENT_REQUEST_CODE_BUBBLE, activityIntent, 0);
+
+    InCallPresenter.getInstance().addInCallUiListener(this);
+    CallList.getInstance().addListener(this);
+    AudioModeProvider.getInstance().addListener(this);
+    audioState = AudioModeProvider.getInstance().getAudioState();
+  }
+
+  public void tearDown() {
+    InCallPresenter.getInstance().removeInCallUiListener(this);
+    CallList.getInstance().removeListener(this);
+    AudioModeProvider.getInstance().removeListener(this);
+  }
+
+  @Override
+  public void onUiShowing(boolean showing) {
+    if (showing) {
+      hide();
+    } else {
+      if (TelecomUtil.isInManagedCall(context)) {
+        show();
+      }
+    }
+  }
+
+  private void hide() {
+    if (bubble != null) {
+      bubble.hide();
+    } else {
+      LogUtil.i("ReturnToCallController.hide", "hide() called without calling show()");
+    }
+  }
+
+  private void hideAndReset() {
+    if (bubble != null) {
+      bubble.hideAndReset();
+    } else {
+      LogUtil.i("ReturnToCallController.reset", "reset() called without calling show()");
+    }
+  }
+
+  private void show() {
+    if (bubble == null) {
+      bubble = startBubble();
+    } else {
+      bubble.show();
+    }
+  }
+
+  @VisibleForTesting
+  public NewBubble startBubble() {
+    if (!NewBubble.canShowBubbles(context)) {
+      LogUtil.i("ReturnToCallController.startNewBubble", "can't show bubble, no permission");
+      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;
+  }
+
+  @Override
+  public void onIncomingCall(DialerCall call) {}
+
+  @Override
+  public void onUpgradeToVideo(DialerCall call) {}
+
+  @Override
+  public void onSessionModificationStateChange(DialerCall call) {}
+
+  @Override
+  public void onCallListChange(CallList callList) {}
+
+  @Override
+  public void onDisconnect(DialerCall call) {
+    if (call.wasParentCall()) {
+      // It's disconnected after the last child call is disconnected, and we already did everything
+      // for the last child.
+      LogUtil.i(
+          "ReturnToCallController.onDisconnect", "being called for a parent call and do nothing");
+      return;
+    }
+    if (bubble != null
+        && bubble.isVisible()
+        && (!TelecomUtil.isInManagedCall(context)
+            || CallList.getInstance().getActiveOrBackgroundCall() != null)) {
+      bubble.showText(context.getText(R.string.incall_call_ended));
+    }
+    // For conference call, we should hideAndReset for the last disconnected child call while the
+    // parent call is still there.
+    if (!CallList.getInstance().hasNonParentActiveOrBackgroundCall()) {
+      hideAndReset();
+    }
+  }
+
+  @Override
+  public void onWiFiToLteHandover(DialerCall call) {}
+
+  @Override
+  public void onHandoverToWifiFailed(DialerCall call) {}
+
+  @Override
+  public void onInternationalCallOnWifi(@NonNull DialerCall call) {}
+
+  @Override
+  public void onAudioStateChanged(CallAudioState audioState) {
+    this.audioState = audioState;
+    if (bubble != null) {
+      bubble.updateActions(generateActions());
+    }
+  }
+
+  private NewBubbleInfo generateBubbleInfo() {
+    return NewBubbleInfo.builder()
+        .setPrimaryColor(context.getResources().getColor(R.color.dialer_theme_color, null))
+        .setPrimaryIcon(Icon.createWithResource(context, R.drawable.on_going_call))
+        .setStartingYPosition(
+            context.getResources().getDimensionPixelOffset(R.dimen.return_to_call_initial_offset_y))
+        .setActions(generateActions())
+        .build();
+  }
+
+  @NonNull
+  private List<Action> generateActions() {
+    List<Action> actions = new ArrayList<>();
+    SpeakerButtonInfo speakerButtonInfo = new SpeakerButtonInfo(audioState, IconSize.SIZE_24_DP);
+
+    actions.add(
+        Action.builder()
+            .setIconDrawable(context.getDrawable(R.drawable.quantum_ic_fullscreen_vd_theme_24))
+            .setIntent(fullScreen)
+            .build());
+    actions.add(
+        Action.builder()
+            .setIconDrawable(context.getDrawable(R.drawable.quantum_ic_mic_off_white_24))
+            .setChecked(audioState.isMuted())
+            .setIntent(toggleMute)
+            .build());
+    actions.add(
+        Action.builder()
+            .setIconDrawable(context.getDrawable(speakerButtonInfo.icon))
+            .setName(context.getText(speakerButtonInfo.label))
+            .setChecked(speakerButtonInfo.isChecked)
+            .setIntent(speakerButtonInfo.checkable ? toggleSpeaker : showSpeakerSelect)
+            .build());
+    actions.add(
+        Action.builder()
+            .setIconDrawable(context.getDrawable(R.drawable.quantum_ic_call_end_vd_theme_24))
+            .setIntent(endCall)
+            .build());
+    return actions;
+  }
+
+  @NonNull
+  private PendingIntent createActionIntent(String action) {
+    Intent toggleSpeaker = new Intent(context, ReturnToCallActionReceiver.class);
+    toggleSpeaker.setAction(action);
+    return PendingIntent.getBroadcast(context, 0, toggleSpeaker, 0);
+  }
+}
diff --git a/java/com/android/incallui/StatusBarNotifier.java b/java/com/android/incallui/StatusBarNotifier.java
index 7ff0040..4ce4393 100644
--- a/java/com/android/incallui/StatusBarNotifier.java
+++ b/java/com/android/incallui/StatusBarNotifier.java
@@ -376,8 +376,6 @@
           builder.setColorized(true);
           builder.setChannelId(NotificationChannelId.ONGOING_CALL);
         }
-        // This will be ignored on O+ and handled by the channel
-        builder.setPriority(Notification.PRIORITY_MAX);
         break;
       default:
         break;
@@ -648,7 +646,8 @@
       return R.drawable.ic_hd_call;
     }
     // If ReturnToCall is enabled, use the static icon. The animated one will show in the bubble.
-    if (ReturnToCallController.isEnabled(mContext)) {
+    if (ReturnToCallController.isEnabled(mContext)
+        || NewReturnToCallController.isEnabled(mContext)) {
       return R.drawable.quantum_ic_call_vd_theme_24;
     } else {
       return R.drawable.on_going_call;
@@ -714,7 +713,7 @@
 
     if (resId == R.string.notification_incoming_call_wifi_template
         || resId == R.string.notification_ongoing_call_wifi_template) {
-      // TODO(b/64525903): Potentially apply this template logic everywhere.
+      // TODO(a bug): Potentially apply this template logic everywhere.
       return mContext.getString(resId, wifiBrand);
     }
 
@@ -1027,11 +1026,6 @@
 
   @Override
   public void onAudioStateChanged(CallAudioState audioState) {
-    if (CallList.getInstance().getActiveOrBackgroundCall() == null) {
-      // We only care about speaker mode when in call
-      return;
-    }
-
     updateNotification();
   }
 
diff --git a/java/com/android/incallui/VideoCallPresenter.java b/java/com/android/incallui/VideoCallPresenter.java
index ab02c3b..fd775e2 100644
--- a/java/com/android/incallui/VideoCallPresenter.java
+++ b/java/com/android/incallui/VideoCallPresenter.java
@@ -358,7 +358,7 @@
 
     // Ensure that the call's camera direction is updated (most likely to UNKNOWN). Normally this
     // happens after any call state changes but we're unregistering from InCallPresenter above so
-    // we won't get any more call state changes. See b/32957114.
+    // we won't get any more call state changes. See a bug.
     if (mPrimaryCall != null) {
       updateCameraSelection(mPrimaryCall);
     }
@@ -851,7 +851,7 @@
     if (!hasCameraPermission) {
       videoCall.setCamera(null);
       mPreviewSurfaceState = PreviewSurfaceState.NONE;
-      // TODO(wangqi): Inform remote party that the video is off. This is similar to b/30256571.
+      // TODO(wangqi): Inform remote party that the video is off. This is similar to a bug.
     } else if (isCameraRequired) {
       InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
       videoCall.setCamera(cameraManager.getActiveCameraId());
diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
index ea5956c..ebbaef8 100644
--- a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
+++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
@@ -421,9 +421,7 @@
     // spec timeline can be divided into 9 slots. Each slot is equivalent to 83ms in the spec.
     // Therefore, we use 9 slots of 83ms to map user gesture into the spec timeline.
     //
-    // See specs -
-    // Accept: https://direct.googleplex.com/#/spec/8510001
-    // Decline: https://direct.googleplex.com/#/spec/3850001
+
     final float progressSlots = 9;
 
     // Fade out the "swipe up to answer". It only takes 1 slot to complete the fade.
diff --git a/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml b/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml
index 2dc274b..d79da88 100644
--- a/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml
+++ b/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml
@@ -87,7 +87,7 @@
           android:layout_marginEnd="24dp"/>
 
       <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses.
-           b/31396406 -->
+           a bug -->
       <com.android.incallui.autoresizetext.AutoResizeTextView
           android:id="@id/contactgrid_contact_name"
           android:layout_width="wrap_content"
diff --git a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
index 113144b..50f6ae6 100644
--- a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
+++ b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
@@ -90,8 +90,8 @@
     } else {
       // TODO(twyen): choose a wake lock implementation base on framework/device.
       // These bugs requires the PseudoProximityWakeLock workaround:
-      // b/30439151 Proximity sensor not working on M
-      // b/31499931 fautly touch input when screen is off on marlin/sailfish
+      // a bug Proximity sensor not working on M
+      // a bug fautly touch input when screen is off on marlin/sailfish
       answerProximityWakeLock = new SystemProximityWakeLock(context);
     }
     answerProximityWakeLock.setScreenOnListener(this);
diff --git a/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java b/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java
index eda0ee7..676a1a3 100644
--- a/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java
+++ b/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java
@@ -27,7 +27,7 @@
  * new DOWN event once the point started moving and then behave as a normal gesture. To prevent
  * accidental answer/rejects, touches that started when the screen is off should be ignored.
  *
- * <p>b/31499931 on certain devices with N-DR1, if the screen is already touched when the screen is
+ * <p>a bug on certain devices with N-DR1, if the screen is already touched when the screen is
  * turned on, a "DOWN MOVE UP" will be sent for each movement before the touch is actually released.
  * These events is hard to discern from other normal events, and keeping the screen on reduces its'
  * probability.
diff --git a/java/com/android/incallui/audiomode/AudioModeProvider.java b/java/com/android/incallui/audiomode/AudioModeProvider.java
index eb59e95..6bdd239 100644
--- a/java/com/android/incallui/audiomode/AudioModeProvider.java
+++ b/java/com/android/incallui/audiomode/AudioModeProvider.java
@@ -69,7 +69,7 @@
    * Sets a approximated audio state before {@link #onAudioStateChanged} is called. Classes such as
    * {@link com.android.incallui.ProximitySensor} fetches the audio state before it is updated by
    * telecom. This method attempts to guess the correct routing based on connected audio devices.
-   * The audio state may still be wrong on a second call due to b/64811128, telecom setting the
+   * The audio state may still be wrong on a second call due to a bug, telecom setting the
    * route back to earpiece when a call ends.
    */
   public void initializeAudioState(Context context) {
diff --git a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
index 9fd3aed..d481f43 100644
--- a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
+++ b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
@@ -92,7 +92,7 @@
         CallAudioState.ROUTE_EARPIECE,
         audioState);
 
-    // TODO(b/67013452): set peak height correctly to fully expand it in landscape mode.
+    // TODO(a bug): set peak height correctly to fully expand it in landscape mode.
     return view;
   }
 
diff --git a/java/com/android/incallui/autoresizetext/AutoResizeTextView.java b/java/com/android/incallui/autoresizetext/AutoResizeTextView.java
index 5a22b93..2789cea 100644
--- a/java/com/android/incallui/autoresizetext/AutoResizeTextView.java
+++ b/java/com/android/incallui/autoresizetext/AutoResizeTextView.java
@@ -35,7 +35,7 @@
  * A TextView that automatically scales its text to completely fill its allotted width.
  *
  * <p>Note: In some edge cases, the binary search algorithm to find the best fit may slightly
- * overshoot / undershoot its constraints. See b/26704434. No minimal repro case has been
+ * overshoot / undershoot its constraints. See a bug. No minimal repro case has been
  * found yet. A known workaround is the solution provided on StackOverflow:
  * http://stackoverflow.com/a/5535672
  */
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
index fcfb0a6..59f3834 100644
--- a/java/com/android/incallui/call/CallList.java
+++ b/java/com/android/incallui/call/CallList.java
@@ -298,7 +298,7 @@
       manager.unregisterStateChangedListener(call);
 
       // Don't log an already logged call. logCall() might be called multiple times
-      // for the same call due to b/24109437.
+      // for the same call due to a bug.
       if (call.getLogState() != null && !call.getLogState().isLogged) {
         getLegacyBindings(context).logCall(call);
         call.getLogState().isLogged = true;
@@ -344,7 +344,7 @@
       DialerCall call = mCallByTelecomCall.get(telecomCall);
 
       // Don't log an already logged call. logCall() might be called multiple times
-      // for the same call due to b/24109437.
+      // for the same call due to a bug.
       if (call.getLogState() != null && !call.getLogState().isLogged) {
         getLegacyBindings(context).logCall(call);
         call.getLogState().isLogged = true;
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
index d911a4c..e8523d6 100644
--- a/java/com/android/incallui/call/DialerCall.java
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -94,7 +94,7 @@
   public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2;
 
   // Hard coded property for {@code Call}. Upstreamed change from Motorola.
-  // TODO(b/35359461): Move it to Telecom in framework.
+  // TODO(a bug): Move it to Telecom in framework.
   public static final int PROPERTY_CODEC_KNOWN = 0x04000000;
 
   private static final String ID_PREFIX = "DialerCall_";
@@ -143,9 +143,12 @@
   @CallHistoryStatus private int mCallHistoryStatus = CALL_HISTORY_STATUS_UNKNOWN;
   private boolean mIsSpam;
   private boolean mIsBlocked;
-  private boolean isInUserSpamList;
-  private boolean isInUserWhiteList;
-  private boolean isInGlobalSpamList;
+
+  @Nullable private Boolean isInUserSpamList;
+
+  @Nullable private Boolean isInUserWhiteList;
+
+  @Nullable private Boolean isInGlobalSpamList;
   private boolean didShowCameraPermission;
   private String callProviderLabel;
   private String callbackNumber;
@@ -575,7 +578,7 @@
    */
   protected boolean areCallExtrasCorrupted(Bundle callExtras) {
     /**
-     * There's currently a bug in Telephony service (b/25613098) that could corrupt the extras
+     * There's currently a bug in Telephony service (a bug) that could corrupt the extras
      * bundle, resulting in a IllegalArgumentException while validating data under {@link
      * Bundle#containsKey(String)}.
      */
@@ -1015,7 +1018,8 @@
     didShowCameraPermission = didShow;
   }
 
-  public boolean isInGlobalSpamList() {
+  @Nullable
+  public Boolean isInGlobalSpamList() {
     return isInGlobalSpamList;
   }
 
@@ -1023,7 +1027,8 @@
     isInGlobalSpamList = inSpamList;
   }
 
-  public boolean isInUserSpamList() {
+  @Nullable
+  public Boolean isInUserSpamList() {
     return isInUserSpamList;
   }
 
@@ -1031,7 +1036,8 @@
     isInUserSpamList = inSpamList;
   }
 
-  public boolean isInUserWhiteList() {
+  @Nullable
+  public Boolean isInUserWhiteList() {
     return isInUserWhiteList;
   }
 
diff --git a/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
index 305ab43..0884c2f 100644
--- a/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
+++ b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
@@ -30,7 +30,7 @@
 
 /**
  * Helper class to check if Google Location Services is enabled. This class is based on
- * https://docs.google.com/a/google.com/document/d/1sGm8pHgGY1QmxbLCwTZuWQASEDN7CFW9EPSZXAuGQfo
+
  */
 public class GoogleLocationSettingHelper {
 
diff --git a/java/com/android/incallui/calllocation/impl/HttpFetcher.java b/java/com/android/incallui/calllocation/impl/HttpFetcher.java
index a63cbc6..10cc34d 100644
--- a/java/com/android/incallui/calllocation/impl/HttpFetcher.java
+++ b/java/com/android/incallui/calllocation/impl/HttpFetcher.java
@@ -222,7 +222,7 @@
   /**
    * Lookup up url re-write rules from gServices and apply to the given url.
    *
-   * <p>https://wiki.corp.google.com/twiki/bin/view/Main/AndroidGservices#URL_Rewriting_Rules
+
    *
    * @return The new url.
    */
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
index f0504bc..e96060c 100644
--- a/java/com/android/incallui/incall/impl/InCallFragment.java
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -45,6 +45,7 @@
 import com.android.dialer.logging.Logger;
 import com.android.dialer.multimedia.MultimediaData;
 import com.android.dialer.strictmode.StrictModeUtils;
+import com.android.dialer.util.ViewUtil;
 import com.android.dialer.widget.LockableViewPager;
 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment;
 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter;
@@ -174,6 +175,9 @@
               : TelephonyManager.NETWORK_TYPE_UNKNOWN;
     }
     phoneType = getContext().getSystemService(TelephonyManager.class).getPhoneType();
+    View space = view.findViewById(R.id.navigation_bar_background);
+    space.getLayoutParams().height = ViewUtil.getNavigationBarHeight(getContext());
+
     return view;
   }
 
diff --git a/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml b/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml
index 6d85567..8e9120b 100644
--- a/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml
+++ b/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml
@@ -1,5 +1,21 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-  <item android:color="#FF01579B" android:state_checked="true"/>
+  <item android:color="#FF1C3AA9" android:state_checked="true"/>
   <item android:color="#FFFFFFFF"/>
 </selector>
diff --git a/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml b/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml
index c06f709..73b9c26 100644
--- a/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml
+++ b/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml
@@ -15,107 +15,114 @@
   ~ limitations under the License
   -->
 <FrameLayout
-  xmlns:android="http://schemas.android.com/apk/res/android"
-  xmlns:app="http://schemas.android.com/apk/res-auto"
-  xmlns:tools="http://schemas.android.com/tools"
-  android:layout_width="match_parent"
-  android:layout_height="match_parent">
-
-  <RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:clipChildren="false"
-    android:clipToPadding="false"
-    android:fitsSystemWindows="true">
+    android:layout_height="match_parent">
+
+  <RelativeLayout
+      xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto"
+      xmlns:tools="http://schemas.android.com/tools"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:clipChildren="false"
+      android:clipToPadding="false"
+      android:fitsSystemWindows="true">
 
     <LinearLayout
-      android:id="@id/incall_contact_grid"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:layout_marginTop="12dp"
-      android:layout_marginStart="@dimen/incall_window_margin_horizontal"
-      android:layout_marginEnd="@dimen/incall_window_margin_horizontal"
-      android:gravity="center_horizontal"
-      android:orientation="vertical">
+        android:id="@id/incall_contact_grid"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="12dp"
+        android:layout_marginStart="@dimen/incall_window_margin_horizontal"
+        android:layout_marginEnd="@dimen/incall_window_margin_horizontal"
+        android:gravity="center_horizontal"
+        android:orientation="vertical">
 
       <ImageView
-        android:id="@id/contactgrid_avatar"
-        android:layout_width="@dimen/incall_avatar_size"
-        android:layout_height="@dimen/incall_avatar_size"
-        android:layout_marginBottom="8dp"
-        android:elevation="2dp"/>
+          android:id="@id/contactgrid_avatar"
+          android:layout_width="@dimen/incall_avatar_size"
+          android:layout_height="@dimen/incall_avatar_size"
+          android:layout_marginBottom="8dp"
+          android:elevation="2dp"/>
 
       <include
-        layout="@layout/incall_contactgrid_top_row"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"/>
+          layout="@layout/incall_contactgrid_top_row"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"/>
 
       <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses.
-                 b/31396406 -->
+                 a bug -->
       <com.android.incallui.autoresizetext.AutoResizeTextView
-        android:id="@id/contactgrid_contact_name"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginBottom="4dp"
-        android:singleLine="true"
-        android:textAppearance="@style/Dialer.Incall.TextAppearance.Large"
-        app:autoResizeText_minTextSize="28sp"
-        tools:text="Jake Peralta"
-        tools:ignore="Deprecated"/>
+          android:id="@id/contactgrid_contact_name"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:layout_marginBottom="4dp"
+          android:singleLine="true"
+          android:textAppearance="@style/Dialer.Incall.TextAppearance.Large"
+          app:autoResizeText_minTextSize="28sp"
+          tools:ignore="Deprecated"
+          tools:text="Jake Peralta"/>
 
       <include
-        layout="@layout/incall_contactgrid_bottom_row"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"/>
+          layout="@layout/incall_contactgrid_bottom_row"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"/>
 
       <FrameLayout
-        android:id="@+id/incall_location_holder"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"/>
+          android:id="@+id/incall_location_holder"
+          android:layout_width="match_parent"
+          android:layout_height="match_parent"/>
     </LinearLayout>
 
     <com.android.dialer.widget.LockableViewPager
-      android:id="@+id/incall_pager"
-      android:layout_width="match_parent"
-      android:layout_height="match_parent"
-      android:layout_above="@+id/incall_paginator"
-      android:layout_below="@+id/incall_contact_grid"
-      android:layout_centerHorizontal="true"/>
+        android:id="@+id/incall_pager"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_above="@+id/incall_paginator"
+        android:layout_below="@+id/incall_contact_grid"
+        android:layout_centerHorizontal="true"/>
 
     <com.android.incallui.incall.impl.InCallPaginator
         android:id="@+id/incall_paginator"
-        android:layout_height="@dimen/paginator_height"
         android:layout_width="@dimen/paginator_width"
+        android:layout_height="@dimen/paginator_height"
         android:layout_above="@+id/incall_end_call"
         android:layout_centerHorizontal="true"
         android:visibility="gone"/>
 
     <FrameLayout
-      android:id="@+id/incall_dialpad_container"
-      style="@style/DialpadContainer"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:layout_alignParentBottom="true"
-      android:clipChildren="false"
-      android:clipToPadding="false"
-      tools:background="@android:color/white"
-      tools:visibility="gone"/>
+        android:id="@+id/incall_dialpad_container"
+        style="@style/DialpadContainer"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:clipChildren="false"
+        android:clipToPadding="false"
+        tools:background="@android:color/white"
+        tools:visibility="gone"/>
     <ImageButton
-      android:id="@+id/incall_end_call"
-      style="@style/Incall.Button.End"
-      android:layout_marginTop="16dp"
-      android:layout_marginBottom="36dp"
-      android:layout_alignParentBottom="true"
-      android:layout_centerHorizontal="true"
-      android:contentDescription="@string/incall_content_description_end_call"/>
+        android:id="@+id/incall_end_call"
+        style="@style/Incall.Button.End"
+        android:layout_marginTop="16dp"
+        android:layout_marginBottom="36dp"
+        android:layout_alignParentBottom="true"
+        android:layout_centerHorizontal="true"
+        android:contentDescription="@string/incall_content_description_end_call"/>
   </RelativeLayout>
 
   <FrameLayout
-    android:id="@id/incall_on_hold_banner"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:layout_gravity="top"/>
+      android:id="@id/incall_on_hold_banner"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_gravity="top"/>
+  <FrameLayout
+      android:id="@+id/navigation_bar_background"
+      android:layout_width="match_parent"
+      android:layout_height="0dp"
+      android:layout_gravity="bottom"
+      android:visibility="gone"
+      android:background="@android:color/background_dark"/>
 </FrameLayout>
diff --git a/java/com/android/incallui/incall/protocol/PrimaryCallState.java b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
index 4a50fbe..7a3abed 100644
--- a/java/com/android/incallui/incall/protocol/PrimaryCallState.java
+++ b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
@@ -71,7 +71,7 @@
   @Nullable public final String customLabel;
   @Nullable public final TransformationInfo assistedDialingExtras;
 
-  // TODO: Convert to autovalue. b/34502119
+  // TODO: Convert to autovalue. a bug
   public static PrimaryCallState createEmptyPrimaryCallState() {
     return createEmptyPrimaryCallStateWithState(DialerCall.State.IDLE, null);
   }
diff --git a/java/com/android/incallui/incall/protocol/PrimaryInfo.java b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
index 69eee20..f7457c3 100644
--- a/java/com/android/incallui/incall/protocol/PrimaryInfo.java
+++ b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
@@ -45,7 +45,7 @@
   public final boolean showInCallButtonGrid;
   public final int numberPresentation;
 
-  // TODO: Convert to autovalue. b/34502119
+  // TODO: Convert to autovalue. a bug
   public static PrimaryInfo createEmptyPrimaryInfo() {
     return new PrimaryInfo(
         null,
diff --git a/java/com/android/incallui/sessiondata/MultimediaFragment.java b/java/com/android/incallui/sessiondata/MultimediaFragment.java
index 3e6cdbb..6c2490f 100644
--- a/java/com/android/incallui/sessiondata/MultimediaFragment.java
+++ b/java/com/android/incallui/sessiondata/MultimediaFragment.java
@@ -182,7 +182,7 @@
                     boolean isFirstResource) {
                   view.findViewById(R.id.loading_spinner).setVisibility(View.GONE);
                   LogUtil.e("MultimediaFragment.onLoadFailed", null, e);
-                  // TODO(b/34720074) handle error cases nicely
+                  // TODO(a bug) handle error cases nicely
                   return false; // Let Glide handle the rest
                 }
 
diff --git a/java/com/android/incallui/spam/SpamNotificationActivity.java b/java/com/android/incallui/spam/SpamNotificationActivity.java
index 86988ad..f5ea73c 100644
--- a/java/com/android/incallui/spam/SpamNotificationActivity.java
+++ b/java/com/android/incallui/spam/SpamNotificationActivity.java
@@ -274,7 +274,7 @@
 
     logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_BLOCK_NUMBER);
     filteredNumberAsyncQueryHandler.blockNumber(null, number, getCountryIso());
-    // TODO: DialerCall finish() after block/reporting async tasks complete (b/28441936)
+    // TODO: DialerCall finish() after block/reporting async tasks complete (a bug)
     finish();
   }
 
@@ -289,7 +289,7 @@
             CallLog.Calls.INCOMING_TYPE,
             ReportingLocation.Type.FEEDBACK_PROMPT,
             contactLookupResultType);
-    // TODO: DialerCall finish() after async task completes (b/28441936)
+    // TODO: DialerCall finish() after async task completes (a bug)
     finish();
   }
 
diff --git a/java/com/android/incallui/spam/SpamNotificationService.java b/java/com/android/incallui/spam/SpamNotificationService.java
index 91377db..3c1061a 100644
--- a/java/com/android/incallui/spam/SpamNotificationService.java
+++ b/java/com/android/incallui/spam/SpamNotificationService.java
@@ -117,7 +117,7 @@
         break;
       default: // fall out
     }
-    // TODO: call stopSelf() after async tasks complete (b/28441936)
+    // TODO: call stopSelf() after async tasks complete (a bug)
     stopSelf();
     return START_NOT_STICKY;
   }
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java
index b0beb77..a3614c0 100644
--- a/java/com/android/incallui/video/impl/VideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/VideoCallFragment.java
@@ -85,7 +85,7 @@
 import com.android.incallui.videotech.utils.VideoUtils;
 
 /** Contains UI elements for a video call. */
-// LINT.IfChange
+
 public class VideoCallFragment extends Fragment
     implements InCallScreen,
         InCallButtonUi,
@@ -1271,4 +1271,4 @@
     }
   }
 }
-// LINT.ThenChange(//depot/google3/third_party/java_src/android_app/dialer/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java)
+
diff --git a/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml b/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml
index ad984f3..3cf0504 100644
--- a/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml
+++ b/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml
@@ -15,7 +15,7 @@
     android:layout_height="wrap_content"/>
 
   <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses.
-       b/31396406 -->
+       a bug -->
   <com.android.incallui.autoresizetext.AutoResizeTextView
     android:id="@id/contactgrid_contact_name"
     android:layout_width="wrap_content"
diff --git a/java/com/android/newbubble/AndroidManifest.xml b/java/com/android/newbubble/AndroidManifest.xml
new file mode 100644
index 0000000..048f8cf
--- /dev/null
+++ b/java/com/android/newbubble/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<!--
+  ~ 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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.newbubble">
+
+  <uses-sdk android:minSdkVersion="23"/>
+  <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+</manifest>
diff --git a/java/com/android/newbubble/NewBubble.java b/java/com/android/newbubble/NewBubble.java
new file mode 100644
index 0000000..d9b9ae2
--- /dev/null
+++ b/java/com/android/newbubble/NewBubble.java
@@ -0,0 +1,837 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.newbubble;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.annotation.SuppressLint;
+import android.app.PendingIntent.CanceledException;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.RippleDrawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.graphics.ColorUtils;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v4.os.BuildCompat;
+import android.support.v4.view.animation.FastOutLinearInInterpolator;
+import android.support.v4.view.animation.LinearOutSlowInInterpolator;
+import android.transition.TransitionManager;
+import android.transition.TransitionValues;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewPropertyAnimator;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.view.animation.AnticipateInterpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+import com.android.newbubble.NewBubbleInfo.Action;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * Creates and manages a bubble window from information in a {@link NewBubbleInfo}. Before creating,
+ * be sure to check whether bubbles may be shown using {@link #canShowBubbles(Context)} and request
+ * permission if necessary ({@link #getRequestPermissionIntent(Context)} is provided for
+ * convenience)
+ */
+public class NewBubble {
+  // This class has some odd behavior that is not immediately obvious in order to avoid jank when
+  // resizing. See http://go/bubble-resize for details.
+
+  // How long text should show after showText(CharSequence) is called
+  private static final int SHOW_TEXT_DURATION_MILLIS = 3000;
+  // How long the new window should show before destroying the old one during resize operations.
+  // This ensures the new window has had time to draw first.
+  private static final int WINDOW_REDRAW_DELAY_MILLIS = 50;
+
+  private static Boolean canShowBubblesForTesting = null;
+
+  private final Context context;
+  private final WindowManager windowManager;
+
+  private final Handler handler;
+  private LayoutParams windowParams;
+
+  // Initialized in factory method
+  @SuppressWarnings("NullableProblems")
+  @NonNull
+  private NewBubbleInfo currentInfo;
+
+  @Visibility private int visibility;
+  private boolean expanded;
+  private boolean textShowing;
+  private boolean hideAfterText;
+  private CharSequence textAfterShow;
+  private int collapseEndAction;
+
+  @VisibleForTesting ViewHolder viewHolder;
+  private ViewPropertyAnimator collapseAnimation;
+  private Integer overrideGravity;
+  private ViewPropertyAnimator exitAnimator;
+
+  private final Runnable collapseRunnable =
+      new Runnable() {
+        @Override
+        public void run() {
+          textShowing = false;
+          if (hideAfterText) {
+            // Always reset here since text shouldn't keep showing.
+            hideAndReset();
+          } else {
+            doResize(
+                () -> viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_ICON));
+          }
+        }
+      };
+
+  private BubbleExpansionStateListener bubbleExpansionStateListener;
+
+  /** Type of action after bubble collapse */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE})
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  public @interface CollapseEnd {
+    int NOTHING = 0;
+    int HIDE = 1;
+  }
+
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({Visibility.ENTERING, Visibility.SHOWING, Visibility.EXITING, Visibility.HIDDEN})
+  private @interface Visibility {
+    int HIDDEN = 0;
+    int ENTERING = 1;
+    int SHOWING = 2;
+    int EXITING = 3;
+  }
+
+  /** Indicate bubble expansion state. */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({ExpansionState.START_EXPANDING, ExpansionState.START_COLLAPSING})
+  public @interface ExpansionState {
+    // TODO(yueg): add more states when needed
+    int START_EXPANDING = 0;
+    int START_COLLAPSING = 1;
+  }
+
+  /**
+   * Determines whether bubbles can be shown based on permissions obtained. This should be checked
+   * before attempting to create a Bubble.
+   *
+   * @return true iff bubbles are able to be shown.
+   * @see Settings#canDrawOverlays(Context)
+   */
+  public static boolean canShowBubbles(@NonNull Context context) {
+    return canShowBubblesForTesting != null
+        ? canShowBubblesForTesting
+        : Settings.canDrawOverlays(context);
+  }
+
+  @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+  public static void setCanShowBubblesForTesting(boolean canShowBubbles) {
+    canShowBubblesForTesting = canShowBubbles;
+  }
+
+  /** Returns an Intent to request permission to show overlays */
+  @NonNull
+  public static Intent getRequestPermissionIntent(@NonNull Context context) {
+    return new Intent(
+        Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+        Uri.fromParts("package", context.getPackageName(), null));
+  }
+
+  /** Creates instances of Bubble. The default implementation just calls the constructor. */
+  @VisibleForTesting
+  public interface BubbleFactory {
+    NewBubble createBubble(@NonNull Context context, @NonNull Handler handler);
+  }
+
+  private static BubbleFactory bubbleFactory = NewBubble::new;
+
+  public static NewBubble createBubble(@NonNull Context context, @NonNull NewBubbleInfo info) {
+    NewBubble bubble = bubbleFactory.createBubble(context, new Handler());
+    bubble.setBubbleInfo(info);
+    return bubble;
+  }
+
+  @VisibleForTesting
+  public static void setBubbleFactory(@NonNull BubbleFactory bubbleFactory) {
+    NewBubble.bubbleFactory = bubbleFactory;
+  }
+
+  @VisibleForTesting
+  public static void resetBubbleFactory() {
+    NewBubble.bubbleFactory = NewBubble::new;
+  }
+
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  NewBubble(@NonNull Context context, @NonNull Handler handler) {
+    context = new ContextThemeWrapper(context, R.style.Theme_AppCompat);
+    this.context = context;
+    this.handler = handler;
+    windowManager = context.getSystemService(WindowManager.class);
+
+    viewHolder = new ViewHolder(context);
+  }
+
+  /** Expands the main bubble menu. */
+  public void expand(boolean isUserAction) {
+    if (bubbleExpansionStateListener != null) {
+      bubbleExpansionStateListener.onBubbleExpansionStateChanged(
+          ExpansionState.START_EXPANDING, isUserAction);
+    }
+    doResize(() -> viewHolder.setDrawerVisibility(View.VISIBLE));
+    View expandedView = viewHolder.getExpandedView();
+    expandedView
+        .getViewTreeObserver()
+        .addOnPreDrawListener(
+            new OnPreDrawListener() {
+              @Override
+              public boolean onPreDraw() {
+                // Animate expanded view to move from above primary button to its final position
+                expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
+                expandedView.setTranslationY(-viewHolder.getRoot().getHeight());
+                expandedView
+                    .animate()
+                    .setInterpolator(new LinearOutSlowInInterpolator())
+                    .translationY(0);
+                return false;
+              }
+            });
+    setFocused(true);
+    expanded = true;
+  }
+
+  /**
+   * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is
+   * already showing this method does nothing.
+   */
+  public void show() {
+    if (collapseEndAction == CollapseEnd.HIDE) {
+      // If show() was called while collapsing, make sure we don't hide after.
+      collapseEndAction = CollapseEnd.NOTHING;
+    }
+    if (visibility == Visibility.SHOWING || visibility == Visibility.ENTERING) {
+      return;
+    }
+
+    hideAfterText = false;
+
+    if (windowParams == null) {
+      // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O.
+      @SuppressWarnings("deprecation")
+      @SuppressLint("InlinedApi")
+      int type =
+          BuildCompat.isAtLeastO()
+              ? LayoutParams.TYPE_APPLICATION_OVERLAY
+              : LayoutParams.TYPE_PHONE;
+
+      windowParams =
+          new LayoutParams(
+              type,
+              LayoutParams.FLAG_NOT_TOUCH_MODAL
+                  | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+                  | LayoutParams.FLAG_NOT_FOCUSABLE
+                  | 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.y = currentInfo.getStartingYPosition();
+      windowParams.height = LayoutParams.WRAP_CONTENT;
+      windowParams.width = LayoutParams.WRAP_CONTENT;
+    }
+
+    if (exitAnimator != null) {
+      exitAnimator.cancel();
+      exitAnimator = null;
+    } else {
+      windowManager.addView(viewHolder.getRoot(), windowParams);
+      viewHolder.getPrimaryButton().setScaleX(0);
+      viewHolder.getPrimaryButton().setScaleY(0);
+    }
+
+    viewHolder.setChildClickable(true);
+    visibility = Visibility.ENTERING;
+    viewHolder
+        .getPrimaryButton()
+        .animate()
+        .setInterpolator(new OvershootInterpolator())
+        .scaleX(1)
+        .scaleY(1)
+        .withEndAction(
+            () -> {
+              visibility = Visibility.SHOWING;
+              // Show the queued up text, if available.
+              if (textAfterShow != null) {
+                showText(textAfterShow);
+                textAfterShow = null;
+              }
+            })
+        .start();
+
+    updatePrimaryIconAnimation();
+  }
+
+  /** Hide the bubble. */
+  public void hide() {
+    if (hideAfterText) {
+      // hideAndReset() will be called after showing text, do nothing here.
+      return;
+    }
+    hideHelper(this::defaultAfterHidingAnimation);
+  }
+
+  /** Hide the bubble and reset {@viewHolder} to initial state */
+  public void hideAndReset() {
+    hideHelper(
+        () -> {
+          defaultAfterHidingAnimation();
+          reset();
+        });
+  }
+
+  /** Returns whether the bubble is currently visible */
+  public boolean isVisible() {
+    return visibility == Visibility.SHOWING
+        || visibility == Visibility.ENTERING
+        || visibility == Visibility.EXITING;
+  }
+
+  /**
+   * Set the info for this Bubble to display
+   *
+   * @param bubbleInfo the BubbleInfo to display in this Bubble.
+   */
+  public void setBubbleInfo(@NonNull NewBubbleInfo bubbleInfo) {
+    currentInfo = bubbleInfo;
+    update();
+  }
+
+  /**
+   * Update the state and behavior of actions.
+   *
+   * @param actions the new state of the bubble's actions
+   */
+  public void updateActions(@NonNull List<Action> actions) {
+    currentInfo = NewBubbleInfo.from(currentInfo).setActions(actions).build();
+    updateButtonStates();
+  }
+
+  /** Returns the currently displayed NewBubbleInfo */
+  public NewBubbleInfo getBubbleInfo() {
+    return currentInfo;
+  }
+
+  /**
+   * Display text in the main bubble. The bubble's drawer is not expandable while text is showing,
+   * and the drawer will be closed if already open.
+   *
+   * @param text the text to display to the user
+   */
+  public void showText(@NonNull CharSequence text) {
+    textShowing = true;
+    if (expanded) {
+      startCollapse(CollapseEnd.NOTHING, false);
+      doShowText(text);
+    } else {
+      // Need to transition from old bounds to new bounds manually
+      NewChangeOnScreenBounds transition = new NewChangeOnScreenBounds();
+      // Prepare and capture start values
+      TransitionValues startValues = new TransitionValues();
+      startValues.view = viewHolder.getPrimaryButton();
+      transition.addTarget(startValues.view);
+      transition.captureStartValues(startValues);
+
+      // If our view is not laid out yet, postpone showing the text.
+      if (startValues.values.isEmpty()) {
+        textAfterShow = text;
+        return;
+      }
+
+      doResize(
+          () -> {
+            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);
+
+                        // 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 text in
+                        Animator alpha =
+                            ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f);
+
+                        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);
+    viewHolder
+        .getPrimaryButton()
+        .animate()
+        .translationZ(
+            context.getResources().getDimensionPixelOffset(R.dimen.bubble_move_elevation_change));
+  }
+
+  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() {
+    if (textShowing || currentInfo.getActions().isEmpty()) {
+      return;
+    }
+    if (expanded) {
+      startCollapse(CollapseEnd.NOTHING, true);
+    } else {
+      expand(true);
+    }
+  }
+
+  LayoutParams getWindowParams() {
+    return windowParams;
+  }
+
+  View getRootView() {
+    return viewHolder.getRoot();
+  }
+
+  /**
+   * Hide the bubble if visible. Will run a short exit animation and before hiding, and {@code
+   * afterHiding} after hiding. If the bubble is currently showing text, will hide after the text is
+   * done displaying. If the bubble is not visible this method does nothing.
+   */
+  private void hideHelper(Runnable afterHiding) {
+    if (visibility == Visibility.HIDDEN || visibility == Visibility.EXITING) {
+      return;
+    }
+
+    // Make bubble non clickable to prevent further buggy actions
+    viewHolder.setChildClickable(false);
+
+    if (textShowing) {
+      hideAfterText = true;
+      return;
+    }
+
+    if (collapseAnimation != null) {
+      collapseEndAction = CollapseEnd.HIDE;
+      return;
+    }
+
+    if (expanded) {
+      startCollapse(CollapseEnd.HIDE, false);
+      return;
+    }
+
+    visibility = Visibility.EXITING;
+    exitAnimator =
+        viewHolder
+            .getPrimaryButton()
+            .animate()
+            .setInterpolator(new AnticipateInterpolator())
+            .scaleX(0)
+            .scaleY(0)
+            .withEndAction(afterHiding);
+    exitAnimator.start();
+  }
+
+  private void reset() {
+    viewHolder = new ViewHolder(viewHolder.getRoot().getContext());
+    update();
+  }
+
+  private void update() {
+    RippleDrawable backgroundRipple =
+        (RippleDrawable)
+            context.getResources().getDrawable(R.drawable.bubble_ripple_circle, context.getTheme());
+    int primaryTint =
+        ColorUtils.compositeColors(
+            context.getColor(R.color.bubble_primary_background_darken),
+            currentInfo.getPrimaryColor());
+    backgroundRipple.getDrawable(0).setTint(primaryTint);
+    viewHolder.getPrimaryButton().setBackground(backgroundRipple);
+
+    viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon());
+    updatePrimaryIconAnimation();
+
+    updateButtonStates();
+  }
+
+  private void updatePrimaryIconAnimation() {
+    Drawable drawable = viewHolder.getPrimaryIcon().getDrawable();
+    if (drawable instanceof Animatable) {
+      if (isVisible()) {
+        ((Animatable) drawable).start();
+      } else {
+        ((Animatable) drawable).stop();
+      }
+    }
+  }
+
+  private void updateButtonStates() {
+    int colorBlue = context.getColor(R.color.bubble_button_text_color_blue);
+    int colorWhite = context.getColor(R.color.bubble_button_text_color_white);
+
+    configureButton(currentInfo.getActions().get(0), viewHolder.getFullScreenButton(), colorBlue);
+    configureButton(currentInfo.getActions().get(1), viewHolder.getMuteButton(), colorBlue);
+    configureButton(currentInfo.getActions().get(2), viewHolder.getAudioRouteButton(), colorBlue);
+    configureButton(currentInfo.getActions().get(3), viewHolder.getEndCallButton(), colorWhite);
+  }
+
+  private void doShowText(@NonNull CharSequence text) {
+    TransitionManager.beginDelayedTransition((ViewGroup) viewHolder.getPrimaryButton().getParent());
+    viewHolder.getPrimaryText().setText(text);
+    viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_TEXT);
+  }
+
+  private void configureButton(Action action, NewCheckableButton button, @ColorInt int iconColor) {
+    Drawable iconDrawable = DrawableCompat.wrap(action.getIconDrawable());
+    DrawableCompat.setTint(iconDrawable.mutate(), iconColor);
+
+    button.setCompoundDrawablesWithIntrinsicBounds(iconDrawable, null, null, null);
+    button.setChecked(action.isChecked());
+    button.setEnabled(action.isEnabled());
+    if (action.getName() != null) {
+      button.setText(action.getName());
+    }
+    button.setOnClickListener(v -> doAction(action));
+  }
+
+  private void doAction(Action action) {
+    try {
+      action.getIntent().send();
+    } catch (CanceledException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  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.
+    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();
+    }
+
+    if (isDrawingFromRight()) {
+      swapViewHolders(oldViewHolder);
+    }
+  }
+
+  private void swapViewHolders(ViewHolder oldViewHolder) {
+    ViewGroup root = viewHolder.getRoot();
+    windowManager.addView(root, windowParams);
+    root.getViewTreeObserver()
+        .addOnPreDrawListener(
+            new OnPreDrawListener() {
+              @Override
+              public boolean onPreDraw() {
+                root.getViewTreeObserver().removeOnPreDrawListener(this);
+                // Wait a bit before removing the old view; make sure the new one has drawn over it.
+                handler.postDelayed(
+                    () -> windowManager.removeView(oldViewHolder.getRoot()),
+                    WINDOW_REDRAW_DELAY_MILLIS);
+                return true;
+              }
+            });
+  }
+
+  @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);
+                });
+  }
+
+  private boolean isDrawingFromRight() {
+    return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
+  }
+
+  private void setFocused(boolean focused) {
+    if (focused) {
+      windowParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
+    } else {
+      windowParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
+    }
+    windowManager.updateViewLayout(getRootView(), windowParams);
+  }
+
+  private void defaultAfterHidingAnimation() {
+    exitAnimator = null;
+    windowManager.removeView(viewHolder.getRoot());
+    visibility = Visibility.HIDDEN;
+
+    updatePrimaryIconAnimation();
+  }
+
+  @VisibleForTesting
+  class ViewHolder {
+
+    public static final int CHILD_INDEX_ICON = 0;
+    public static final int CHILD_INDEX_TEXT = 1;
+
+    private final NewMoveHandler moveHandler;
+    private final NewWindowRoot root;
+    private final ViewAnimator primaryButton;
+    private final ImageView primaryIcon;
+    private final TextView primaryText;
+
+    private final NewCheckableButton fullScreenButton;
+    private final NewCheckableButton muteButton;
+    private final NewCheckableButton audioRouteButton;
+    private final NewCheckableButton endCallButton;
+    private final View expandedView;
+
+    public ViewHolder(Context context) {
+      // Window root is not in the layout file so that the inflater has a view to inflate into
+      this.root = new NewWindowRoot(context);
+      LayoutInflater inflater = LayoutInflater.from(root.getContext());
+      View contentView = inflater.inflate(R.layout.new_bubble_base, root, true);
+      expandedView = contentView.findViewById(R.id.bubble_expanded_layout);
+      primaryButton = contentView.findViewById(R.id.bubble_button_primary);
+      primaryIcon = contentView.findViewById(R.id.bubble_icon_primary);
+      primaryText = contentView.findViewById(R.id.bubble_text);
+
+      fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen);
+      muteButton = contentView.findViewById(R.id.bubble_button_mute);
+      audioRouteButton = contentView.findViewById(R.id.bubble_button_audio_route);
+      endCallButton = contentView.findViewById(R.id.bubble_button_end_call);
+
+      root.setOnBackPressedListener(
+          () -> {
+            if (visibility == Visibility.SHOWING && expanded) {
+              startCollapse(CollapseEnd.NOTHING, true);
+              return true;
+            }
+            return false;
+          });
+      root.setOnConfigurationChangedListener(
+          (configuration) -> {
+            // The values in the current MoveHandler may be stale, so replace it. Then ensure the
+            // Window is in bounds
+            moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
+            moveHandler.snapToBounds();
+          });
+      root.setOnTouchListener(
+          (v, event) -> {
+            if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
+              startCollapse(CollapseEnd.NOTHING, true);
+              return true;
+            }
+            return false;
+          });
+      moveHandler = new NewMoveHandler(primaryButton, NewBubble.this);
+    }
+
+    private void setChildClickable(boolean clickable) {
+      fullScreenButton.setClickable(clickable);
+      muteButton.setClickable(clickable);
+      audioRouteButton.setClickable(clickable);
+      endCallButton.setClickable(clickable);
+
+      // For primaryButton
+      moveHandler.setClickable(clickable);
+    }
+
+    public ViewGroup getRoot() {
+      return root;
+    }
+
+    public ViewAnimator getPrimaryButton() {
+      return primaryButton;
+    }
+
+    public ImageView getPrimaryIcon() {
+      return primaryIcon;
+    }
+
+    public TextView getPrimaryText() {
+      return primaryText;
+    }
+
+    public View getExpandedView() {
+      return expandedView;
+    }
+
+    public NewCheckableButton getFullScreenButton() {
+      return fullScreenButton;
+    }
+
+    public NewCheckableButton getMuteButton() {
+      return muteButton;
+    }
+
+    public NewCheckableButton getAudioRouteButton() {
+      return audioRouteButton;
+    }
+
+    public NewCheckableButton getEndCallButton() {
+      return endCallButton;
+    }
+
+    public void setDrawerVisibility(int visibility) {
+      expandedView.setVisibility(visibility);
+    }
+
+    public boolean isMoving() {
+      return moveHandler.isMoving();
+    }
+
+    public void undoGravityOverride() {
+      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/NewBubbleInfo.java b/java/com/android/newbubble/NewBubbleInfo.java
new file mode 100644
index 0000000..f615929
--- /dev/null
+++ b/java/com/android/newbubble/NewBubbleInfo.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.newbubble;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.Px;
+import com.google.auto.value.AutoValue;
+import java.util.Collections;
+import java.util.List;
+
+/** Info for displaying a {@link NewBubble} */
+@AutoValue
+public abstract class NewBubbleInfo {
+  @ColorInt
+  public abstract int getPrimaryColor();
+
+  public abstract Icon getPrimaryIcon();
+
+  @Px
+  public abstract int getStartingYPosition();
+
+  @NonNull
+  public abstract List<Action> getActions();
+
+  public static Builder builder() {
+    return new AutoValue_NewBubbleInfo.Builder().setActions(Collections.emptyList());
+  }
+
+  public static Builder from(@NonNull NewBubbleInfo bubbleInfo) {
+    return builder()
+        .setPrimaryColor(bubbleInfo.getPrimaryColor())
+        .setPrimaryIcon(bubbleInfo.getPrimaryIcon())
+        .setStartingYPosition(bubbleInfo.getStartingYPosition())
+        .setActions(bubbleInfo.getActions());
+  }
+
+  /** Builder for {@link NewBubbleInfo} */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder setPrimaryColor(@ColorInt int primaryColor);
+
+    public abstract Builder setPrimaryIcon(@NonNull Icon primaryIcon);
+
+    public abstract Builder setStartingYPosition(@Px int startingYPosition);
+
+    public abstract Builder setActions(List<Action> actions);
+
+    public abstract NewBubbleInfo build();
+  }
+
+  /** Represents actions to be shown in the bubble when expanded */
+  @AutoValue
+  public abstract static class Action {
+
+    public abstract Drawable getIconDrawable();
+
+    @Nullable
+    public abstract CharSequence getName();
+
+    @NonNull
+    public abstract PendingIntent getIntent();
+
+    public abstract boolean isEnabled();
+
+    public abstract boolean isChecked();
+
+    public static Builder builder() {
+      return new AutoValue_NewBubbleInfo_Action.Builder().setEnabled(true).setChecked(false);
+    }
+
+    public static Builder from(@NonNull Action action) {
+      return builder()
+          .setIntent(action.getIntent())
+          .setChecked(action.isChecked())
+          .setEnabled(action.isEnabled())
+          .setName(action.getName())
+          .setIconDrawable(action.getIconDrawable());
+    }
+
+    /** Builder for {@link Action} */
+    @AutoValue.Builder
+    public abstract static class Builder {
+
+      public abstract Builder setIconDrawable(Drawable iconDrawable);
+
+      public abstract Builder setName(@Nullable CharSequence name);
+
+      public abstract Builder setIntent(@NonNull PendingIntent intent);
+
+      public abstract Builder setEnabled(boolean enabled);
+
+      public abstract Builder setChecked(boolean checked);
+
+      public abstract Action build();
+    }
+  }
+}
diff --git a/java/com/android/newbubble/NewChangeOnScreenBounds.java b/java/com/android/newbubble/NewChangeOnScreenBounds.java
new file mode 100644
index 0000000..0653d3a
--- /dev/null
+++ b/java/com/android/newbubble/NewChangeOnScreenBounds.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.newbubble;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.support.annotation.VisibleForTesting;
+import android.transition.Transition;
+import android.transition.TransitionValues;
+import android.util.Property;
+import android.view.View;
+import android.view.ViewGroup;
+
+/** Similar to {@link android.transition.ChangeBounds ChangeBounds} but works across windows */
+public class NewChangeOnScreenBounds extends Transition {
+
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  static final String PROPNAME_BOUNDS = "bubble:changeScreenBounds:bounds";
+
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  static final String PROPNAME_SCREEN_X = "bubble:changeScreenBounds:screenX";
+
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  static final String PROPNAME_SCREEN_Y = "bubble:changeScreenBounds:screenY";
+
+  static final String PROPNAME_WIDTH = "bubble:changeScreenBounds:width";
+  static final String PROPNAME_HEIGHT = "bubble:changeScreenBounds:height";
+
+  private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY =
+      new Property<ViewBounds, PointF>(PointF.class, "topLeft") {
+        @Override
+        public void set(ViewBounds viewBounds, PointF topLeft) {
+          viewBounds.setTopLeft(topLeft);
+        }
+
+        @Override
+        public PointF get(ViewBounds viewBounds) {
+          return null;
+        }
+      };
+
+  private static final Property<ViewBounds, PointF> BOTTOM_RIGHT_PROPERTY =
+      new Property<ViewBounds, PointF>(PointF.class, "bottomRight") {
+        @Override
+        public void set(ViewBounds viewBounds, PointF bottomRight) {
+          viewBounds.setBottomRight(bottomRight);
+        }
+
+        @Override
+        public PointF get(ViewBounds viewBounds) {
+          return null;
+        }
+      };
+  private final int[] tempLocation = new int[2];
+
+  @Override
+  public void captureStartValues(TransitionValues transitionValues) {
+    captureValuesWithSize(transitionValues);
+  }
+
+  @Override
+  public void captureEndValues(TransitionValues transitionValues) {
+    captureValuesWithSize(transitionValues);
+  }
+
+  /**
+   * Capture location (left and top) from {@code values.view} and size (width and height) from
+   * {@code values.values}. If size is not set, use the size of {@code values.view}.
+   */
+  private void captureValuesWithSize(TransitionValues values) {
+    View view = values.view;
+
+    if (view.isLaidOut() || view.getWidth() != 0 || view.getHeight() != 0) {
+      Integer width = (Integer) values.values.get(PROPNAME_WIDTH);
+      Integer height = (Integer) values.values.get(PROPNAME_HEIGHT);
+
+      values.values.put(
+          PROPNAME_BOUNDS,
+          new Rect(
+              view.getLeft(),
+              view.getTop(),
+              width == null ? view.getRight() : view.getLeft() + width,
+              height == null ? view.getBottom() : view.getTop() + height));
+      values.view.getLocationOnScreen(tempLocation);
+      values.values.put(PROPNAME_SCREEN_X, tempLocation[0]);
+      values.values.put(PROPNAME_SCREEN_Y, tempLocation[1]);
+    }
+  }
+
+  @Override
+  public Animator createAnimator(
+      ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
+    Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
+    Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
+
+    if (startBounds == null || endBounds == null) {
+      // start or end values were not captured, so don't animate.
+      return null;
+    }
+
+    // Offset the startBounds by the difference in screen position
+    int startScreenX = (Integer) startValues.values.get(PROPNAME_SCREEN_X);
+    int startScreenY = (Integer) startValues.values.get(PROPNAME_SCREEN_Y);
+    int endScreenX = (Integer) endValues.values.get(PROPNAME_SCREEN_X);
+    int endScreenY = (Integer) endValues.values.get(PROPNAME_SCREEN_Y);
+    startBounds.offset(startScreenX - endScreenX, startScreenY - endScreenY);
+
+    final int startLeft = startBounds.left;
+    final int endLeft = endBounds.left;
+    final int startTop = startBounds.top;
+    final int endTop = endBounds.top;
+    final int startRight = startBounds.right;
+    final int endRight = endBounds.right;
+    final int startBottom = startBounds.bottom;
+    final int endBottom = endBounds.bottom;
+    ViewBounds viewBounds = new ViewBounds(endValues.view);
+    viewBounds.setTopLeft(new PointF(startLeft, startTop));
+    viewBounds.setBottomRight(new PointF(startRight, startBottom));
+
+    // Animate the top left and bottom right corners along a path
+    Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft, endTop);
+    ObjectAnimator topLeftAnimator =
+        ObjectAnimator.ofObject(viewBounds, TOP_LEFT_PROPERTY, null, topLeftPath);
+
+    Path bottomRightPath = getPathMotion().getPath(startRight, startBottom, endRight, endBottom);
+    ObjectAnimator bottomRightAnimator =
+        ObjectAnimator.ofObject(viewBounds, BOTTOM_RIGHT_PROPERTY, null, bottomRightPath);
+    AnimatorSet set = new AnimatorSet();
+    set.playTogether(topLeftAnimator, bottomRightAnimator);
+    return set;
+  }
+
+  private static class ViewBounds {
+    private int left;
+    private int top;
+    private int right;
+    private int bottom;
+    private final View view;
+    private int topLeftCalls;
+    private int bottomRightCalls;
+
+    public ViewBounds(View view) {
+      this.view = view;
+    }
+
+    public void setTopLeft(PointF topLeft) {
+      left = Math.round(topLeft.x);
+      top = Math.round(topLeft.y);
+      topLeftCalls++;
+      if (topLeftCalls == bottomRightCalls) {
+        updateLeftTopRightBottom();
+      }
+    }
+
+    public void setBottomRight(PointF bottomRight) {
+      right = Math.round(bottomRight.x);
+      bottom = Math.round(bottomRight.y);
+      bottomRightCalls++;
+      if (topLeftCalls == bottomRightCalls) {
+        updateLeftTopRightBottom();
+      }
+    }
+
+    private void updateLeftTopRightBottom() {
+      view.setLeft(left);
+      view.setTop(top);
+      view.setRight(right);
+      view.setBottom(bottom);
+      topLeftCalls = 0;
+      bottomRightCalls = 0;
+    }
+  }
+}
diff --git a/java/com/android/newbubble/NewCheckableButton.java b/java/com/android/newbubble/NewCheckableButton.java
new file mode 100644
index 0000000..63525a4
--- /dev/null
+++ b/java/com/android/newbubble/NewCheckableButton.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.newbubble;
+
+import android.content.Context;
+import android.support.v4.view.AccessibilityDelegateCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v7.widget.AppCompatButton;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.Checkable;
+
+/**
+ * A {@link android.widget.Button Button} that implements {@link Checkable} and propagates the
+ * checkable state
+ */
+public class NewCheckableButton extends AppCompatButton implements Checkable {
+
+  private boolean mChecked;
+
+  public NewCheckableButton(Context context) {
+    this(context, null);
+  }
+
+  public NewCheckableButton(Context context, AttributeSet attrs) {
+    this(context, attrs, android.R.attr.imageButtonStyle);
+  }
+
+  public NewCheckableButton(Context context, AttributeSet attrs, int defStyleAttr) {
+    super(context, attrs, defStyleAttr);
+
+    ViewCompat.setAccessibilityDelegate(
+        this,
+        new AccessibilityDelegateCompat() {
+          @Override
+          public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+            super.onInitializeAccessibilityEvent(host, event);
+            event.setChecked(isChecked());
+          }
+
+          @Override
+          public void onInitializeAccessibilityNodeInfo(
+              View host, AccessibilityNodeInfoCompat info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+            info.setCheckable(true);
+            info.setChecked(isChecked());
+          }
+        });
+  }
+
+  @Override
+  public void setChecked(boolean checked) {
+    if (mChecked != checked) {
+      mChecked = checked;
+      setTextColor(
+          checked
+              ? getContext().getColor(R.color.bubble_button_text_color_blue)
+              : getContext().getColor(R.color.bubble_button_text_color_black));
+    }
+  }
+
+  @Override
+  public boolean isChecked() {
+    return mChecked;
+  }
+
+  @Override
+  public void toggle() {
+    setChecked(!mChecked);
+  }
+}
diff --git a/java/com/android/newbubble/NewMoveHandler.java b/java/com/android/newbubble/NewMoveHandler.java
new file mode 100644
index 0000000..189ad84
--- /dev/null
+++ b/java/com/android/newbubble/NewMoveHandler.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.newbubble;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.support.animation.FloatPropertyCompat;
+import android.support.animation.SpringAnimation;
+import android.support.animation.SpringForce;
+import android.support.annotation.NonNull;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.widget.Scroller;
+
+/** Handles touches and manages moving the bubble in response */
+class NewMoveHandler implements OnTouchListener {
+
+  // Amount the ViewConfiguration's minFlingVelocity will be scaled by for our own minVelocity
+  private static final int MIN_FLING_VELOCITY_FACTOR = 8;
+  // The friction multiplier to control how slippery the bubble is when flung
+  private static final float SCROLL_FRICTION_MULTIPLIER = 4f;
+
+  private final Context context;
+  private final WindowManager windowManager;
+  private final NewBubble bubble;
+  private final int minX;
+  private final int minY;
+  private final int maxX;
+  private final int maxY;
+  private final int bubbleSize;
+  private final float touchSlopSquared;
+
+  private boolean clickable = true;
+  private boolean isMoving;
+  private float firstX;
+  private float firstY;
+
+  private SpringAnimation moveXAnimation;
+  private SpringAnimation moveYAnimation;
+  private VelocityTracker velocityTracker;
+  private Scroller scroller;
+
+  private static float clamp(float value, float min, float max) {
+    return Math.min(max, Math.max(min, value));
+  }
+
+  // Handles the left/right gravity conversion and centering
+  private final FloatPropertyCompat<WindowManager.LayoutParams> xProperty =
+      new FloatPropertyCompat<LayoutParams>("xProperty") {
+        @Override
+        public float getValue(LayoutParams windowParams) {
+          int realX = windowParams.x;
+          realX = realX + bubbleSize / 2;
+          if (relativeToRight(windowParams)) {
+            int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
+            realX = displayWidth - realX;
+          }
+          return clamp(realX, minX, maxX);
+        }
+
+        @Override
+        public void setValue(LayoutParams windowParams, float value) {
+          int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
+          boolean onRight;
+          Integer gravityOverride = bubble.getGravityOverride();
+          if (gravityOverride == null) {
+            onRight = value > displayWidth / 2;
+          } else {
+            onRight = (gravityOverride & Gravity.RIGHT) == Gravity.RIGHT;
+          }
+          int centeringOffset = bubbleSize / 2;
+          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);
+          }
+        }
+      };
+
+  private final FloatPropertyCompat<WindowManager.LayoutParams> yProperty =
+      new FloatPropertyCompat<LayoutParams>("yProperty") {
+        @Override
+        public float getValue(LayoutParams object) {
+          return clamp(object.y + bubbleSize, minY, maxY);
+        }
+
+        @Override
+        public void setValue(LayoutParams object, float value) {
+          object.y = (int) value - bubbleSize;
+          if (bubble.isVisible()) {
+            windowManager.updateViewLayout(bubble.getRootView(), object);
+          }
+        }
+      };
+
+  public NewMoveHandler(@NonNull View targetView, @NonNull NewBubble bubble) {
+    this.bubble = bubble;
+    context = targetView.getContext();
+    windowManager = context.getSystemService(WindowManager.class);
+
+    bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
+    minX =
+        context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_horizontal)
+            + bubbleSize / 2;
+    minY =
+        context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_vertical)
+            + bubbleSize / 2;
+    maxX = context.getResources().getDisplayMetrics().widthPixels - minX;
+    maxY = context.getResources().getDisplayMetrics().heightPixels - minY;
+
+    // Squared because it will be compared against the square of the touch delta. This is more
+    // efficient than needing to take a square root.
+    touchSlopSquared = (float) Math.pow(ViewConfiguration.get(context).getScaledTouchSlop(), 2);
+
+    targetView.setOnTouchListener(this);
+  }
+
+  public void setClickable(boolean clickable) {
+    this.clickable = clickable;
+  }
+
+  public boolean isMoving() {
+    return isMoving;
+  }
+
+  public void undoGravityOverride() {
+    LayoutParams windowParams = bubble.getWindowParams();
+    xProperty.setValue(windowParams, xProperty.getValue(windowParams));
+  }
+
+  public void snapToBounds() {
+    ensureSprings();
+
+    moveXAnimation.animateToFinalPosition(relativeToRight(bubble.getWindowParams()) ? maxX : minX);
+    moveYAnimation.animateToFinalPosition(yProperty.getValue(bubble.getWindowParams()));
+  }
+
+  @Override
+  public boolean onTouch(View v, MotionEvent event) {
+    float eventX = event.getRawX();
+    float eventY = event.getRawY();
+    switch (event.getActionMasked()) {
+      case MotionEvent.ACTION_DOWN:
+        firstX = eventX;
+        firstY = eventY;
+        velocityTracker = VelocityTracker.obtain();
+        break;
+      case MotionEvent.ACTION_MOVE:
+        if (isMoving || hasExceededTouchSlop(event)) {
+          if (!isMoving) {
+            isMoving = true;
+            bubble.onMoveStart();
+          }
+
+          ensureSprings();
+
+          moveXAnimation.animateToFinalPosition(clamp(eventX, minX, maxX));
+          moveYAnimation.animateToFinalPosition(clamp(eventY, minY, maxY));
+        }
+
+        velocityTracker.addMovement(event);
+        break;
+      case MotionEvent.ACTION_UP:
+        if (isMoving) {
+          ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
+          velocityTracker.computeCurrentVelocity(
+              1000, viewConfiguration.getScaledMaximumFlingVelocity());
+          float xVelocity = velocityTracker.getXVelocity();
+          float yVelocity = velocityTracker.getYVelocity();
+          boolean isFling = isFling(xVelocity, yVelocity);
+
+          if (isFling) {
+            Point target =
+                findTarget(
+                    xVelocity,
+                    yVelocity,
+                    (int) xProperty.getValue(bubble.getWindowParams()),
+                    (int) yProperty.getValue(bubble.getWindowParams()));
+
+            moveXAnimation.animateToFinalPosition(target.x);
+            moveYAnimation.animateToFinalPosition(target.y);
+          } else {
+            snapX();
+          }
+          isMoving = false;
+          bubble.onMoveFinish();
+        } else {
+          v.performClick();
+          if (clickable) {
+            bubble.primaryButtonClick();
+          }
+        }
+        break;
+      default: // fall out
+    }
+    return true;
+  }
+
+  private void ensureSprings() {
+    if (moveXAnimation == null) {
+      moveXAnimation = new SpringAnimation(bubble.getWindowParams(), xProperty);
+      moveXAnimation.setSpring(new SpringForce());
+      moveXAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
+    }
+
+    if (moveYAnimation == null) {
+      moveYAnimation = new SpringAnimation(bubble.getWindowParams(), yProperty);
+      moveYAnimation.setSpring(new SpringForce());
+      moveYAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
+    }
+  }
+
+  private Point findTarget(float xVelocity, float yVelocity, int startX, int startY) {
+    if (scroller == null) {
+      scroller = new Scroller(context);
+      scroller.setFriction(ViewConfiguration.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER);
+    }
+
+    // Find where a fling would end vertically
+    scroller.fling(startX, startY, (int) xVelocity, (int) yVelocity, minX, maxX, minY, maxY);
+    int targetY = scroller.getFinalY();
+    scroller.abortAnimation();
+
+    // If the x component of the velocity is above the minimum fling velocity, use velocity to
+    // determine edge. Otherwise use its starting position
+    boolean pullRight = isFling(xVelocity, 0) ? xVelocity > 0 : isOnRightHalf(startX);
+    return new Point(pullRight ? maxX : minX, targetY);
+  }
+
+  private boolean isFling(float xVelocity, float yVelocity) {
+    int minFlingVelocity =
+        ViewConfiguration.get(context).getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_FACTOR;
+    return getMagnitudeSquared(xVelocity, yVelocity) > minFlingVelocity * minFlingVelocity;
+  }
+
+  private boolean isOnRightHalf(float currentX) {
+    return currentX > (minX + maxX) / 2;
+  }
+
+  private void snapX() {
+    // Check if x value is closer to min or max
+    boolean pullRight = isOnRightHalf(xProperty.getValue(bubble.getWindowParams()));
+    moveXAnimation.animateToFinalPosition(pullRight ? maxX : minX);
+  }
+
+  private boolean relativeToRight(LayoutParams windowParams) {
+    return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
+  }
+
+  private boolean hasExceededTouchSlop(MotionEvent event) {
+    return getMagnitudeSquared(event.getRawX() - firstX, event.getRawY() - firstY)
+        > touchSlopSquared;
+  }
+
+  private float getMagnitudeSquared(float deltaX, float deltaY) {
+    return deltaX * deltaX + deltaY * deltaY;
+  }
+}
diff --git a/java/com/android/newbubble/NewWindowRoot.java b/java/com/android/newbubble/NewWindowRoot.java
new file mode 100644
index 0000000..da24b71
--- /dev/null
+++ b/java/com/android/newbubble/NewWindowRoot.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.newbubble;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.support.annotation.NonNull;
+import android.view.KeyEvent;
+import android.widget.FrameLayout;
+
+/**
+ * ViewGroup that handles some overlay window concerns. Allows back button and configuration change
+ * events to be listened for via interfaces.
+ */
+public class NewWindowRoot extends FrameLayout {
+
+  /** Callback for when the back button is pressed while this window is in focus */
+  public interface OnBackPressedListener {
+    boolean onBackPressed();
+  }
+
+  /** Callback for when the Configuration changes for this window */
+  public interface OnConfigurationChangedListener {
+    void onConfigurationChanged(Configuration newConfiguration);
+  }
+
+  private OnBackPressedListener backPressedListener;
+  private OnConfigurationChangedListener configurationChangedListener;
+
+  public NewWindowRoot(@NonNull Context context) {
+    super(context);
+  }
+
+  public void setOnBackPressedListener(OnBackPressedListener listener) {
+    backPressedListener = listener;
+  }
+
+  public void setOnConfigurationChangedListener(OnConfigurationChangedListener listener) {
+    configurationChangedListener = listener;
+  }
+
+  @Override
+  public boolean dispatchKeyEvent(KeyEvent event) {
+    if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && backPressedListener != null) {
+      if (event.getAction() == KeyEvent.ACTION_UP) {
+        return backPressedListener.onBackPressed();
+      }
+      return true;
+    }
+    return super.dispatchKeyEvent(event);
+  }
+
+  @Override
+  public void dispatchConfigurationChanged(Configuration newConfig) {
+    super.dispatchConfigurationChanged(newConfig);
+    if (configurationChangedListener != null) {
+      configurationChangedListener.onConfigurationChanged(newConfig);
+    }
+  }
+}
diff --git a/java/com/android/newbubble/res/drawable/bubble_background_with_radius.xml b/java/com/android/newbubble/res/drawable/bubble_background_with_radius.xml
new file mode 100644
index 0000000..4fd871c
--- /dev/null
+++ b/java/com/android/newbubble/res/drawable/bubble_background_with_radius.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+  <corners
+      android:bottomRightRadius="@dimen/bubble_radius"
+      android:topRightRadius="@dimen/bubble_radius"
+      android:bottomLeftRadius="@dimen/bubble_radius"
+      android:topLeftRadius="@dimen/bubble_radius"/>
+  <solid android:color="@android:color/white"/>
+</shape>
diff --git a/java/com/android/newbubble/res/drawable/bubble_ripple_circle.xml b/java/com/android/newbubble/res/drawable/bubble_ripple_circle.xml
new file mode 100644
index 0000000..8d5cf0b
--- /dev/null
+++ b/java/com/android/newbubble/res/drawable/bubble_ripple_circle.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
+  <item>
+    <shape>
+      <corners android:radius="@dimen/bubble_size"/>
+      <solid android:color="@android:color/white"/>
+    </shape>
+  </item>
+</ripple>
diff --git a/java/com/android/newbubble/res/layout/new_bubble_base.xml b/java/com/android/newbubble/res/layout/new_bubble_base.xml
new file mode 100644
index 0000000..ef35d74
--- /dev/null
+++ b/java/com/android/newbubble/res/layout/new_bubble_base.xml
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    tools:theme="@style/Theme.AppCompat">
+  <RelativeLayout
+      android:id="@+id/bubble_primary_container"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_centerHorizontal="true"
+      android:animateLayoutChanges="true"
+      android:clipChildren="false"
+      android:clipToPadding="false"
+      android:elevation="12dp">
+    <ViewAnimator
+        android:id="@+id/bubble_button_primary"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/bubble_ripple_circle"
+        android:measureAllChildren="false"
+        tools:backgroundTint="#FF0000AA">
+      <ImageView
+          android:id="@+id/bubble_icon_primary"
+          android:layout_width="@dimen/bubble_size"
+          android:layout_height="@dimen/bubble_size"
+          android:padding="@dimen/bubble_icon_padding"
+          android:tint="@android:color/white"
+          android:tintMode="src_in"
+          tools:src="@android:drawable/ic_btn_speak_now"/>
+      <TextView
+          android:id="@+id/bubble_text"
+          android:layout_width="wrap_content"
+          android:layout_height="@dimen/bubble_size"
+          android:paddingStart="@dimen/bubble_icon_padding"
+          android:paddingEnd="@dimen/bubble_icon_padding"
+          android:gravity="center"
+          android:minWidth="@dimen/bubble_size"
+          android:textAppearance="@style/TextAppearance.AppCompat"
+          tools:text="Call ended"/>
+    </ViewAnimator>
+  </RelativeLayout>
+  <RelativeLayout
+      android:id="@+id/bubble_expanded_layout"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_below="@id/bubble_primary_container"
+      android:paddingTop="@dimen/bubble_shadow_padding_size_vertical"
+      android:paddingBottom="@dimen/bubble_shadow_padding_size_vertical"
+      android:paddingStart="@dimen/bubble_shadow_padding_size_horizontal"
+      android:paddingEnd="@dimen/bubble_shadow_padding_size_horizontal"
+      android:clipToPadding="false"
+      android:visibility="gone"
+      tools:visibility="visible">
+    <RelativeLayout
+        android:id="@+id/bubble_expanded_layout_part_one"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/bubble_background_with_radius"
+        android:elevation="@dimen/bubble_elevation"
+        android:layoutDirection="inherit">
+      <com.android.newbubble.NewCheckableButton
+          android:id="@+id/bubble_button_full_screen"
+          android:layout_width="@dimen/bubble_expanded_width"
+          android:layout_height="@dimen/bubble_size"
+          android:padding="@dimen/bubble_icon_padding"
+          android:tint="@color/bubble_button_text_color_blue"
+          android:tintMode="src_in"
+          android:text="Full screen"
+          android:textColor="@color/bubble_button_text_color_black"
+          android:background="@android:color/transparent"
+          android:drawablePadding="@dimen/bubble_icon_padding"/>
+      <com.android.newbubble.NewCheckableButton
+          android:id="@+id/bubble_button_mute"
+          android:layout_width="@dimen/bubble_expanded_width"
+          android:layout_height="@dimen/bubble_size"
+          android:layout_below="@id/bubble_button_full_screen"
+          android:padding="@dimen/bubble_icon_padding"
+          android:tint="@color/bubble_button_text_color_blue"
+          android:tintMode="src_in"
+          android:text="Mute"
+          android:textColor="@color/bubble_button_text_color_black"
+          android:background="@android:color/transparent"
+          android:drawablePadding="@dimen/bubble_icon_padding"/>
+      <com.android.newbubble.NewCheckableButton
+          android:id="@+id/bubble_button_audio_route"
+          android:layout_width="@dimen/bubble_expanded_width"
+          android:layout_height="@dimen/bubble_size"
+          android:layout_below="@id/bubble_button_mute"
+          android:padding="@dimen/bubble_icon_padding"
+          android:tint="@color/bubble_button_text_color_blue"
+          android:tintMode="src_in"
+          android:text="Speakerphone"
+          android:textColor="@color/bubble_button_text_color_black"
+          android:background="@android:color/transparent"
+          android:drawablePadding="@dimen/bubble_icon_padding"/>
+    </RelativeLayout>
+    <RelativeLayout
+        android:id="@+id/bubble_expanded_layout_part_two"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/bubble_expanded_separator_height"
+        android:layout_below="@id/bubble_expanded_layout_part_one"
+        android:background="@drawable/bubble_ripple_circle"
+        android:backgroundTint="@color/bubble_end_call_button_background"
+        android:elevation="@dimen/bubble_elevation"
+        android:layoutDirection="inherit">
+      <com.android.newbubble.NewCheckableButton
+          android:id="@+id/bubble_button_end_call"
+          android:layout_width="@dimen/bubble_expanded_width"
+          android:layout_height="@dimen/bubble_size"
+          android:padding="@dimen/bubble_icon_padding"
+          android:tint="@color/bubble_button_text_color_white"
+          android:tintMode="src_in"
+          android:text="End Call"
+          android:textColor="@color/bubble_button_text_color_white"
+          android:background="@android:color/transparent"
+          android:drawablePadding="@dimen/bubble_icon_padding"/>
+    </RelativeLayout>
+  </RelativeLayout>
+</RelativeLayout>
diff --git a/java/com/android/newbubble/res/values/colors.xml b/java/com/android/newbubble/res/values/colors.xml
new file mode 100644
index 0000000..556d8bd
--- /dev/null
+++ b/java/com/android/newbubble/res/values/colors.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<resources>
+  <color name="bubble_primary_background_darken">#33000000</color>
+
+  <color name="bubble_button_text_color_black">@color/dialer_primary_text_color</color>
+  <color name="bubble_button_text_color_white">@color/dialer_primary_text_color_white</color>
+  <color name="bubble_button_text_color_blue">@color/dialer_theme_color</color>
+  <color name="bubble_end_call_button_background">@color/dialer_end_call_button_color</color>
+</resources>
diff --git a/java/com/android/newbubble/res/values/values.xml b/java/com/android/newbubble/res/values/values.xml
new file mode 100644
index 0000000..4bb90af
--- /dev/null
+++ b/java/com/android/newbubble/res/values/values.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<resources>
+  <dimen name="bubble_size">56dp</dimen>
+  <dimen name="bubble_icon_padding">16dp</dimen>
+  <dimen name="bubble_move_elevation_change">4dp</dimen>
+
+  <dimen name="bubble_safe_margin_horizontal">-4dp</dimen>
+  <dimen name="bubble_safe_margin_vertical">64dp</dimen>
+  <dimen name="bubble_shadow_padding_size_vertical">16dp</dimen>
+  <dimen name="bubble_shadow_padding_size_horizontal">12dp</dimen>
+
+  <dimen name="bubble_elevation">10dp</dimen>
+  <dimen name="bubble_expanded_width">160dp</dimen>
+  <dimen name="bubble_radius">20dp</dimen>
+  <dimen name="bubble_expanded_separator_height">4dp</dimen>
+</resources>
diff --git a/java/com/android/voicemail/impl/OmtpService.java b/java/com/android/voicemail/impl/OmtpService.java
index 4db1aeb..4e8860c 100644
--- a/java/com/android/voicemail/impl/OmtpService.java
+++ b/java/com/android/voicemail/impl/OmtpService.java
@@ -25,6 +25,7 @@
 import android.support.annotation.MainThread;
 import android.support.annotation.NonNull;
 import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
 import android.telephony.VisualVoicemailService;
 import android.telephony.VisualVoicemailSms;
 import com.android.dialer.logging.DialerImpression;
@@ -63,6 +64,7 @@
     }
 
     if (!isServiceEnabled(phoneAccountHandle)) {
+      disableFilter(phoneAccountHandle);
       task.finish();
       return;
     }
@@ -87,6 +89,8 @@
     }
 
     if (!isServiceEnabled(sms.getPhoneAccountHandle())) {
+      VvmLog.e(TAG, "onSmsReceived received when service is disabled");
+      disableFilter(sms.getPhoneAccountHandle());
       task.finish();
       return;
     }
@@ -178,6 +182,15 @@
     return true;
   }
 
+  private void disableFilter(PhoneAccountHandle phoneAccountHandle) {
+    TelephonyManager telephonyManager =
+        getSystemService(TelephonyManager.class).createForPhoneAccountHandle(phoneAccountHandle);
+    if (telephonyManager != null) {
+      VvmLog.i(TAG, "disabling SMS filter");
+      telephonyManager.setVisualVoicemailSmsFilterSettings(null);
+    }
+  }
+
   private static boolean isUserUnlocked(@NonNull Context context) {
     UserManager userManager = context.getSystemService(UserManager.class);
     return userManager.isUserUnlocked();
diff --git a/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
index 90303f5..f8dc4bd 100644
--- a/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
+++ b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
@@ -87,6 +87,8 @@
 
   public static final String KEY_VVM_CLIENT_PREFIX_STRING = "vvm_client_prefix_string";
 
+  @Nullable private static PersistableBundle sOverrideConfigForTest;
+
   private final Context mContext;
   private final PersistableBundle mCarrierConfig;
   private final String mVvmType;
@@ -114,16 +116,22 @@
       return;
     }
 
-    if (ConfigOverrideFragment.isOverridden(context)) {
-      mOverrideConfig = ConfigOverrideFragment.getConfig(context);
-      VvmLog.w(TAG, "Config override is activated: " + mOverrideConfig);
+    if (sOverrideConfigForTest != null) {
+      mOverrideConfig = sOverrideConfigForTest;
+      mCarrierConfig = new PersistableBundle();
+      mTelephonyConfig = new PersistableBundle();
     } else {
-      mOverrideConfig = null;
-    }
+      if (ConfigOverrideFragment.isOverridden(context)) {
+        mOverrideConfig = ConfigOverrideFragment.getConfig(context);
+        VvmLog.w(TAG, "Config override is activated: " + mOverrideConfig);
+      } else {
+        mOverrideConfig = null;
+      }
 
-    mCarrierConfig = getCarrierConfig(telephonyManager);
-    mTelephonyConfig =
-        new TelephonyVvmConfigManager(context).getConfig(telephonyManager.getSimOperator());
+      mCarrierConfig = getCarrierConfig(telephonyManager);
+      mTelephonyConfig =
+          new TelephonyVvmConfigManager(context).getConfig(telephonyManager.getSimOperator());
+    }
 
     mVvmType = getVvmType();
     mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
@@ -275,7 +283,7 @@
    * Hidden Config.
    *
    * <p>Sometimes the server states it supports a certain feature but we found they have bug on the
-   * server side. For example, in b/28717550 the server reported AUTH=DIGEST-MD5 capability but
+   * server side. For example, in a bug the server reported AUTH=DIGEST-MD5 capability but
    * using it to login will cause subsequent response to be erroneous.
    *
    * @return A set of capabilities that is reported by the IMAP CAPABILITY command, but determined
@@ -485,4 +493,9 @@
     }
     return defaultValue;
   }
+
+  @VisibleForTesting
+  public static void setOverrideConfigForTest(PersistableBundle config) {
+    sOverrideConfigForTest = config;
+  }
 }
diff --git a/java/com/android/voicemail/impl/VvmPhoneStateListener.java b/java/com/android/voicemail/impl/VvmPhoneStateListener.java
index 00c1358..914120b 100644
--- a/java/com/android/voicemail/impl/VvmPhoneStateListener.java
+++ b/java/com/android/voicemail/impl/VvmPhoneStateListener.java
@@ -24,7 +24,7 @@
 import com.android.voicemail.impl.sync.VvmAccountManager;
 
 /**
- * Check if service is lost and indicate this in the voicemail status. TODO(b/35125657): Not used
+ * Check if service is lost and indicate this in the voicemail status. TODO(a bug): Not used
  * for now, restore it.
  */
 public class VvmPhoneStateListener extends PhoneStateListener {
@@ -36,7 +36,7 @@
   private int mPreviousState = -1;
 
   public VvmPhoneStateListener(Context context, PhoneAccountHandle accountHandle) {
-    // TODO(twyen): b/32637799 too much trouble to call super constructor through reflection,
+    // TODO(twyen): a bug too much trouble to call super constructor through reflection,
     // just use non-phoneAccountHandle version for now.
     super();
     mContext = context;
diff --git a/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java b/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java
index 8bc3cc2..24f530f 100644
--- a/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java
+++ b/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java
@@ -34,7 +34,7 @@
  * Handles {@link OmtpEvents} when {@link Vvm3Protocol} is being used. This handler writes custom
  * error codes into the voicemail status table so support on the dialer side is required.
  *
- * <p>TODO(b/29577838) disable VVM3 by default so support on system dialer can be ensured.
+ * <p>TODO(a bug) disable VVM3 by default so support on system dialer can be ensured.
  */
 public class Vvm3EventHandler {
 
diff --git a/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java b/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java
index fc7fdf3..5454bac 100644
--- a/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java
+++ b/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java
@@ -216,7 +216,7 @@
           new ImapHelper(config.getContext(), phoneAccountHandle, network, status)) {
         // VVM3 has inconsistent error language code to OMTP. Just issue a raw command
         // here.
-        // TODO(b/29082671): use LocaleList
+        // TODO(a bug): use LocaleList
         if (Locale.getDefault().getLanguage().equals(new Locale(ISO639_Spanish).getLanguage())) {
           // Spanish
           helper.changeVoicemailTuiLanguage(VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS);
diff --git a/java/com/android/voicemail/impl/res/xml/vvm_config.xml b/java/com/android/voicemail/impl/res/xml/vvm_config.xml
index d95f315..1e5190a 100644
--- a/java/com/android/voicemail/impl/res/xml/vvm_config.xml
+++ b/java/com/android/voicemail/impl/res/xml/vvm_config.xml
@@ -38,7 +38,7 @@
         value="true"/>
     <!-- Carrier VVM app com.orange.vvm only supports Orange FR-->
     <string-array name="vvm_disabled_capabilities_string_array">
-      <!-- b/32365569 -->
+      <!-- a bug -->
       <item value="STARTTLS"/>
     </string-array>
   </pbundle_as_map>
@@ -62,7 +62,7 @@
       name="vvm_cellular_data_required_bool"
       value="true"/>
     <string-array name="vvm_disabled_capabilities_string_array">
-      <!-- b/32365569 -->
+      <!-- a bug -->
       <item value="STARTTLS"/>
     </string-array>
   </pbundle_as_map>
@@ -101,7 +101,7 @@
     </string-array>
     <string name="vvm_type_string">vvm_type_cvvm</string>>
     <string-array name="vvm_disabled_capabilities_string_array">
-      <!-- b/28717550 -->
+      <!-- a bug -->
       <item value="AUTH=DIGEST-MD5"/>
     </string-array>
   </pbundle_as_map>
diff --git a/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java b/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java
index 5b5d6b0..f7c8f29 100644
--- a/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java
+++ b/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java
@@ -122,7 +122,7 @@
         success = downloadOneVoicemail(imapHelper, voicemail, phoneAccount);
       }
       if (success) {
-        // TODO: b/30569269 failure should interrupt all subsequent task via exceptions
+        // TODO: a bug failure should interrupt all subsequent task via exceptions
         imapHelper.updateQuota();
         autoDeleteAndArchiveVM(imapHelper, phoneAccount);
         imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED);
diff --git a/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java b/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java
index 316e1ca..1af5e68 100644
--- a/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java
+++ b/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java
@@ -83,7 +83,7 @@
   /**
    * Utility method to make queries to the voicemail database.
    *
-   * <p>TODO(b/36588206) add PhoneAccountHandle filtering back
+   * <p>TODO(a bug) add PhoneAccountHandle filtering back
    *
    * @param selection A filter declaring which rows to return. {@code null} returns all rows.
    * @return A list of voicemails according to the selection statement.
diff --git a/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java b/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java
index 068b19b..17f5c16 100644
--- a/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java
+++ b/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java
@@ -128,7 +128,7 @@
 
   @CallSuper
   public void onUnavailable() {
-    // TODO(twyen): b/32637799 this is hidden, do we really need this?
+    // TODO(twyen): a bug this is hidden, do we really need this?
     mResultReceived = true;
     onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
   }
diff --git a/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto b/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto
index 697e9e3..b060170 100644
--- a/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto
+++ b/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto
@@ -1,4 +1,4 @@
-// LINT.IfChange
+
 
 syntax = "proto2";
 
@@ -151,5 +151,4 @@
   }
 }
 
-// LINT.ThenChange(//depot/google3/google/internal/communications/voicemailtranscription/v1/\
-//         voicemail_transcription.proto)
+