Merge "Send notification when permission granted in search" into ub-contactsdialer-a-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 32d88cc..1faab2a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -17,8 +17,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.dialer"
     coreApp="true"
-    android:versionCode="20210"
-    android:versionName="2.21">
+    android:versionCode="20300"
+    android:versionName="2.3">
 
     <uses-sdk
         android:minSdkVersion="23"
diff --git a/res/drawable/tab_contacts.xml b/res/drawable/tab_contacts.xml
deleted file mode 100644
index ed3f86b..0000000
--- a/res/drawable/tab_contacts.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2014 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="@color/tab_ripple_color">
-
-    <item>
-        <bitmap android:src="@drawable/ic_people_24dp"
-            android:gravity="center" />
-    </item>
-
-    <item android:id="@android:id/mask">
-        <color android:color="@android:color/white" />
-    </item>
-
-</ripple>
diff --git a/res/drawable/tab_history.xml b/res/drawable/tab_history.xml
deleted file mode 100644
index b1a5318..0000000
--- a/res/drawable/tab_history.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2014 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="@color/tab_ripple_color">
-
-    <item>
-        <bitmap android:src="@drawable/ic_schedule_24dp"
-            android:gravity="center" />
-    </item>
-
-    <item android:id="@android:id/mask">
-        <color android:color="@android:color/white" />
-    </item>
-
-</ripple>
diff --git a/res/drawable/tab_speed_dial.xml b/res/drawable/tab_speed_dial.xml
deleted file mode 100644
index c9c4820..0000000
--- a/res/drawable/tab_speed_dial.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2014 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="@color/tab_ripple_color">
-
-    <item>
-        <bitmap android:src="@drawable/ic_grade_24dp"
-            android:gravity="center" />
-    </item>
-
-    <item android:id="@android:id/mask">
-        <color android:color="@android:color/white" />
-    </item>
-
-</ripple>
diff --git a/res/drawable/tab_voicemail.xml b/res/drawable/tab_voicemail.xml
deleted file mode 100644
index 568891b..0000000
--- a/res/drawable/tab_voicemail.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2015 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="@color/tab_ripple_color">
-
-    <item>
-        <bitmap android:src="@drawable/ic_voicemail_24dp"
-            android:gravity="center" />
-    </item>
-
-    <item android:id="@android:id/mask">
-        <color android:color="@android:color/white" />
-    </item>
-
-</ripple>
diff --git a/res/layout/voicemail_promo_card.xml b/res/layout/voicemail_promo_card.xml
index 103fa30..891f096 100644
--- a/res/layout/voicemail_promo_card.xml
+++ b/res/layout/voicemail_promo_card.xml
@@ -79,20 +79,20 @@
             android:gravity="end">
 
             <TextView
-                android:id="@+id/settings_action"
+                android:id="@+id/secondary_action"
                 style="@style/PromoCardActionStyle"
                 android:background="?android:attr/selectableItemBackground"
                 android:text="@string/visual_voicemail_settings"
                 android:nextFocusLeft="@+id/promo_card"
-                android:nextFocusRight="@+id/ok_action"
+                android:nextFocusRight="@+id/primary_action"
                 android:paddingEnd="@dimen/promo_card_action_between_padding"/>
 
             <TextView
-                android:id="@+id/ok_action"
+                android:id="@+id/primary_action"
                 style="@style/PromoCardActionStyle"
                 android:background="?android:attr/selectableItemBackground"
                 android:text="@android:string/ok"
-                android:nextFocusLeft="@+id/settings_action"
+                android:nextFocusLeft="@+id/secondary_action"
                 android:nextFocusRight="@+id/promo_card"/>
         </LinearLayout>
     </LinearLayout>
diff --git a/res/menu/call_details_options.xml b/res/menu/call_details_options.xml
index 84cc2c9..0e9e5c9 100644
--- a/res/menu/call_details_options.xml
+++ b/res/menu/call_details_options.xml
@@ -13,7 +13,8 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/call_details_menu">
 
     <item android:id="@+id/menu_trash"
         android:icon="@drawable/ic_delete_24dp"
diff --git a/res/values/ids.xml b/res/values/ids.xml
index cb2025a..0034fe3 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -18,5 +18,6 @@
     <item type="id" name="context_menu_copy_to_clipboard" />
     <item type="id" name="context_menu_copy_transcript_to_clipboard" />
     <item type="id" name="context_menu_edit_before_call" />
+    <item type="id" name="context_menu_block_number" />
     <item type="id" name="settings_header_sounds_and_vibration" />
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1ce6ab6..7bd88ba 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -52,6 +52,24 @@
     <!-- Option displayed in context menu to copy long pressed voicemail transcription to clipboard [CHAR LIMIT=64] -->
     <string name="copy_transcript_text">Copy transcription to clipboard</string>
 
+    <!-- Menu item used to block a number from the call log [CHAR LIMIT=64] -->
+    <string name="call_log_block_number">Block number</string>
+
+    <!-- Text for snackbar to undo blocking a number. [CHAR LIMIT=64] -->
+    <string name="snackbar_number_blocked">
+        <xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g> added to block list</string>
+
+    <!-- Menu item used to unblock a number from the call log [CHAR LIMIT=64]-->
+    <string name="call_log_unblock_number">Unblock number</string>
+
+    <!-- Text for snackbar to undo unblocking a number. [CHAR LIMIT=64] -->
+    <string name="snackbar_number_unblocked">
+        <xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g>
+        removed from block list</string>
+
+    <!-- Text for undo button in snackbar for blocking/unblocking number. [CHAR LIMIT=10] -->
+    <string name="block_number_undo">UNDO</string>
+
     <!-- Menu item used to copy a number from the call log to the dialer so it can be edited before calling it -->
     <string name="call_log_edit_number_before_call">Edit number before call</string>
 
@@ -490,6 +508,20 @@
          [CHAR LIMIT=30] -->
     <string name="call_log_voicemail_title">Voicemail</string>
 
+    <!-- Confirmation dialog for blocking a number. [CHAR LIMIT=NONE] -->
+    <string name="blockNumberConfirmation">Add
+        <xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g> to your block list?</string>
+
+    <!-- Block number alert dialog button [CHAR LIMIT=32] -->
+    <string name="blockNumberOk">Block number</string>
+
+    <!-- Confirmation dialog for unblocking a number. [CHAR LIMIT=NONE] -->
+    <string name="unblockNumberConfirmation">Remove
+        <xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g> from your block list?</string>
+
+    <!-- Unblock number alert dialog button [CHAR LIMIT=32] -->
+    <string name="unblockNumberOk">Unblock number</string>
+
     <!-- Accessibility text for the tab showing recent and favorite contacts who can be called.
          [CHAR LIMIT=40] -->
     <string name="tab_speed_dial">Speed dial</string>
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index ae1a160..4ade04a 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -693,6 +693,14 @@
     }
 
     /**
+     * Update the number of unread voicemails (potentially other tabs) displayed next to the tab
+     * icon.
+     */
+    public void updateTabUnreadCounts() {
+        mListsFragment.updateTabUnreadCounts();
+    }
+
+    /**
      * Initiates a fragment transaction to show the dialpad fragment. Animations and other visual
      * updates are handled by a callback which is invoked after the dialpad fragment is shown.
      * @see #onDialpadShown
@@ -907,11 +915,11 @@
             return;
         }
 
-        final boolean phoneIsInUse = phoneIsInUse();
-        if (phoneIsInUse || (intent.getData() !=  null && isDialIntent(intent))) {
+        final boolean showDialpadChooser = phoneIsInUse() && !DialpadFragment.isAddCallMode(intent);
+        if (showDialpadChooser || (intent.getData() != null && isDialIntent(intent))) {
             showDialpadFragment(false);
             mDialpadFragment.setStartedFromNewIntent(true);
-            if (phoneIsInUse && !mDialpadFragment.isVisible()) {
+            if (showDialpadChooser && !mDialpadFragment.isVisible()) {
                 mInCallDialpadUp = true;
             }
         }
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index 4593e5e..90985af 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -22,12 +22,10 @@
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
-import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.support.v7.widget.RecyclerView;
 import android.os.Bundle;
 import android.os.Trace;
-import android.preference.PreferenceActivity;
 import android.preference.PreferenceManager;
 import android.provider.CallLog;
 import android.support.v7.widget.RecyclerView.ViewHolder;
@@ -35,28 +33,18 @@
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
-import android.util.Log;
-import android.view.ContextMenu;
 import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.MenuItem.OnMenuItemClickListener;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.ContextMenu.ContextMenuInfo;
 import android.view.accessibility.AccessibilityEvent;
-import android.widget.TextView;
 
-import com.android.contacts.common.CallUtil;
-import com.android.contacts.common.ClipboardUtils;
 import com.android.contacts.common.util.PermissionsUtil;
-import com.android.dialer.DialtactsActivity;
 import com.android.dialer.PhoneCallDetails;
 import com.android.dialer.R;
 import com.android.dialer.contactinfo.ContactInfoCache;
 import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
-import com.android.dialer.util.DialerUtils;
+import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
 import com.android.dialer.util.PhoneNumberUtil;
 import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
 
@@ -79,11 +67,9 @@
     private static final int NO_EXPANDED_LIST_ITEM = -1;
 
     private static final int VOICEMAIL_PROMO_CARD_POSITION = 0;
-    /**
-     * View type for voicemail promo card.  Note: Numbering starts at 20 to avoid collision
-     * with {@link com.android.common.widget.GroupingListAdapter#ITEM_TYPE_IN_GROUP}.
-     */
-    private static final int VIEW_TYPE_VOICEMAIL_PROMO_CARD = 20;
+
+    protected static final int VIEW_TYPE_NORMAL = 0;
+    private static final int VIEW_TYPE_VOICEMAIL_PROMO_CARD = 1;
 
     /**
      * The key for the show voicemail promo card preference which will determine whether the promo
@@ -96,6 +82,7 @@
     private final ContactInfoHelper mContactInfoHelper;
     protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
     private final CallFetcher mCallFetcher;
+    private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
 
     protected ContactInfoCache mContactInfoCache;
 
@@ -195,89 +182,6 @@
         }
     };
 
-    /**
-     * Listener that is triggered to populate the context menu with actions to perform on the call's
-     * number, when the call log entry is long pressed.
-     */
-    private final View.OnCreateContextMenuListener mOnCreateContextMenuListener =
-            new View.OnCreateContextMenuListener() {
-                @Override
-                public void onCreateContextMenu(
-                        ContextMenu menu, View v, ContextMenuInfo menuInfo) {
-                    final CallLogListItemViewHolder vh = (CallLogListItemViewHolder) v.getTag();
-                    if (TextUtils.isEmpty(vh.number)) {
-                        return;
-                    }
-
-                    if (vh.callType == CallLog.Calls.VOICEMAIL_TYPE) {
-                        menu.setHeaderTitle(mContext.getResources().getText(R.string.voicemail));
-                    } else {
-                        menu.setHeaderTitle(vh.number);
-                    }
-
-                    final MenuItem copyItem = menu.add(
-                            ContextMenu.NONE,
-                            R.id.context_menu_copy_to_clipboard,
-                            ContextMenu.NONE,
-                            R.string.copy_number_text);
-
-                    copyItem.setOnMenuItemClickListener(new OnMenuItemClickListener() {
-                        @Override
-                        public boolean onMenuItemClick(MenuItem item) {
-                            ClipboardUtils.copyText(mContext, null, vh.number, true);
-                            return true;
-                        }
-                    });
-
-                    // The edit number before call does not show up if any of the conditions apply:
-                    // 1) Number cannot be called
-                    // 2) Number is the voicemail number
-                    // 3) Number is a SIP address
-
-                    if (PhoneNumberUtil.canPlaceCallsTo(vh.number, vh.numberPresentation)
-                            && !mTelecomCallLogCache.isVoicemailNumber(vh.accountHandle, vh.number)
-                            && !PhoneNumberUtil.isSipNumber(vh.number)) {
-                        final MenuItem editItem = menu.add(
-                                ContextMenu.NONE,
-                                R.id.context_menu_edit_before_call,
-                                ContextMenu.NONE,
-                                R.string.call_log_edit_number_before_call);
-
-                        editItem.setOnMenuItemClickListener(new OnMenuItemClickListener() {
-                            @Override
-                            public boolean onMenuItemClick(MenuItem item) {
-                                final Intent intent = new Intent(
-                                        Intent.ACTION_DIAL, CallUtil.getCallUri(vh.number));
-                                intent.setClass(mContext, DialtactsActivity.class);
-                                DialerUtils.startActivityWithErrorToast(mContext, intent);
-                                return true;
-                            }
-                        });
-                    }
-
-                    final TextView transcriptView =
-                            vh.phoneCallDetailsViews.voicemailTranscriptionView;
-                    if (vh.callType == CallLog.Calls.VOICEMAIL_TYPE
-                            && transcriptView.length() > 0) {
-                        final MenuItem copyTranscriptItem = menu.add(
-                                ContextMenu.NONE,
-                                R.id.context_menu_copy_transcript_to_clipboard,
-                                ContextMenu.NONE,
-                                R.string.copy_transcript_text);
-
-                        copyTranscriptItem.setOnMenuItemClickListener(
-                                new OnMenuItemClickListener() {
-                                    @Override
-                                    public boolean onMenuItemClick(MenuItem item) {
-                                        ClipboardUtils.copyText(
-                                                mContext, null, transcriptView.getText(), true);
-                                        return true;
-                                    }
-                                });
-                    }
-                }
-            };
-
     private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
         // If another item is expanded, notify it that it has changed. Its actions will be
         // hidden when it is re-binded because we change mCurrentlyExpandedPosition below.
@@ -351,6 +255,8 @@
         mCallLogGroupBuilder = new CallLogGroupBuilder(this);
         mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
         maybeShowVoicemailPromoCard();
+        mFilteredNumberAsyncQueryHandler =
+                new FilteredNumberAsyncQueryHandler(mContext.getContentResolver());
     }
 
     public void onSaveInstanceState(Bundle outState) {
@@ -439,12 +345,12 @@
                 mExpandCollapseListener,
                 mTelecomCallLogCache,
                 mCallLogListItemHelper,
-                mVoicemailPlaybackPresenter);
+                mVoicemailPlaybackPresenter,
+                mFilteredNumberAsyncQueryHandler);
 
         viewHolder.callLogEntryView.setTag(viewHolder);
         viewHolder.callLogEntryView.setAccessibilityDelegate(mAccessibilityDelegate);
 
-        viewHolder.primaryActionView.setOnCreateContextMenuListener(mOnCreateContextMenuListener);
         viewHolder.primaryActionView.setTag(viewHolder);
 
         return viewHolder;
@@ -481,9 +387,9 @@
     protected void bindVoicemailPromoCardViewHolder(ViewHolder viewHolder) {
         PromoCardViewHolder promoCardViewHolder = (PromoCardViewHolder) viewHolder;
 
-        promoCardViewHolder.getSettingsTextView().setOnClickListener(
-                mVoicemailSettingsActionListener);
-        promoCardViewHolder.getOkTextView().setOnClickListener(mOkActionListener);
+        promoCardViewHolder.getSecondaryActionView()
+                .setOnClickListener(mVoicemailSettingsActionListener);
+        promoCardViewHolder.getPrimaryActionView().setOnClickListener(mOkActionListener);
     }
 
     /**
diff --git a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
index 1becc89..5a62a7d 100644
--- a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
+++ b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
@@ -29,6 +29,7 @@
 import android.util.Log;
 
 import com.android.contacts.common.GeoUtil;
+import com.android.dialer.DialtactsActivity;
 import com.android.dialer.PhoneCallDetails;
 import com.android.dialer.util.AsyncTaskExecutor;
 import com.android.dialer.util.AsyncTaskExecutors;
@@ -246,6 +247,8 @@
                 Intent intent = new Intent(context, CallLogNotificationsService.class);
                 intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
                 context.startService(intent);
+
+                ((DialtactsActivity) context).updateTabUnreadCounts();
                 return null;
             }
         });
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 09e4f02..8762f18 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -284,6 +284,9 @@
     }
 
     @Override
+    public void onVoicemailUnreadCountFetched(Cursor cursor) {}
+
+    @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
         View view = inflater.inflate(R.layout.call_log_fragment, container, false);
 
diff --git a/src/com/android/dialer/calllog/CallLogListItemHelper.java b/src/com/android/dialer/calllog/CallLogListItemHelper.java
index 1c8e397..d18e274 100644
--- a/src/com/android/dialer/calllog/CallLogListItemHelper.java
+++ b/src/com/android/dialer/calllog/CallLogListItemHelper.java
@@ -74,6 +74,9 @@
         // Cache name or number of caller.  Used when setting the content descriptions of buttons
         // when the actions ViewStub is inflated.
         views.nameOrNumber = getNameOrNumber(details);
+
+        // Cache country iso. Used for number filtering.
+        views.countryIso = details.countryIso;
     }
 
     /**
diff --git a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
index d85deb3..521b2a4 100644
--- a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
+++ b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
@@ -21,12 +21,15 @@
 import android.content.res.Resources;
 import android.content.Intent;
 import android.net.Uri;
+import android.provider.CallLog;
 import android.provider.CallLog.Calls;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.support.v7.widget.CardView;
 import android.support.v7.widget.RecyclerView;
 import android.telecom.PhoneAccountHandle;
 import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
@@ -34,12 +37,17 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.ClipboardUtils;
 import com.android.contacts.common.ContactPhotoManager;
 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
 import com.android.contacts.common.dialog.CallSubjectDialog;
 import com.android.contacts.common.testing.NeededForTesting;
 import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.DialtactsActivity;
 import com.android.dialer.R;
+import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.filterednumber.FilterNumberDialogFragment;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.PhoneNumberUtil;
 import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
@@ -52,7 +60,8 @@
  * This object also contains UI logic pertaining to the view, to isolate it from the CallLogAdapter.
  */
 public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
-        implements View.OnClickListener {
+        implements View.OnClickListener, MenuItem.OnMenuItemClickListener,
+        View.OnCreateContextMenuListener {
 
     /** The root view of the call log list item */
     public final View rootView;
@@ -116,12 +125,24 @@
     public String numberType;
 
     /**
+     * The country iso for the call. Cached here as the call back
+     * intent is set only when the actions ViewStub is inflated.
+     */
+    public String countryIso;
+
+    /**
      * The type of call for the current call log entry.  Cached here as the call back
      * intent is set only when the actions ViewStub is inflated.
      */
     public int callType;
 
     /**
+     * ID for blocked numbers database.
+     * Set when context menu is created, if the number is blocked.
+     */
+    public Integer blockId;
+
+    /**
      * The account for the current call log entry.  Cached here as the call back
      * intent is set only when the actions ViewStub is inflated.
      */
@@ -156,6 +177,7 @@
     private final TelecomCallLogCache mTelecomCallLogCache;
     private final CallLogListItemHelper mCallLogListItemHelper;
     private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+    private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
 
     private final int mPhotoSize;
 
@@ -168,6 +190,7 @@
             TelecomCallLogCache telecomCallLogCache,
             CallLogListItemHelper callLogListItemHelper,
             VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+            FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
             View rootView,
             QuickContactBadge quickContactView,
             View primaryActionView,
@@ -182,6 +205,7 @@
         mTelecomCallLogCache = telecomCallLogCache;
         mCallLogListItemHelper = callLogListItemHelper;
         mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
+        mFilteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler;
 
         this.rootView = rootView;
         this.quickContactView = quickContactView;
@@ -202,6 +226,7 @@
 
         primaryActionButtonView.setOnClickListener(this);
         primaryActionView.setOnClickListener(mExpandCollapseListener);
+        primaryActionView.setOnCreateContextMenuListener(this);
     }
 
     public static CallLogListItemViewHolder create(
@@ -210,7 +235,8 @@
             View.OnClickListener expandCollapseListener,
             TelecomCallLogCache telecomCallLogCache,
             CallLogListItemHelper callLogListItemHelper,
-            VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
+            VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+            FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) {
 
         return new CallLogListItemViewHolder(
                 context,
@@ -218,6 +244,7 @@
                 telecomCallLogCache,
                 callLogListItemHelper,
                 voicemailPlaybackPresenter,
+                filteredNumberAsyncQueryHandler,
                 view,
                 (QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
                 view.findViewById(R.id.primary_action_view),
@@ -227,6 +254,92 @@
                 (ImageView) view.findViewById(R.id.primary_action_button));
     }
 
+    @Override
+    public void onCreateContextMenu(
+            final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+        if (TextUtils.isEmpty(number)) {
+            return;
+        }
+
+        if (callType == CallLog.Calls.VOICEMAIL_TYPE) {
+            menu.setHeaderTitle(mContext.getResources().getText(R.string.voicemail));
+        } else {
+            menu.setHeaderTitle(number);
+        }
+
+        menu.add(ContextMenu.NONE, R.id.context_menu_copy_to_clipboard, ContextMenu.NONE,
+                R.string.copy_number_text)
+                .setOnMenuItemClickListener(this);
+
+        // The edit number before call does not show up if any of the conditions apply:
+        // 1) Number cannot be called
+        // 2) Number is the voicemail number
+        // 3) Number is a SIP address
+
+        if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation)
+                && !mTelecomCallLogCache.isVoicemailNumber(accountHandle, number)
+                && !PhoneNumberUtil.isSipNumber(number)) {
+            menu.add(ContextMenu.NONE, R.id.context_menu_edit_before_call, ContextMenu.NONE,
+                    R.string.call_log_edit_number_before_call)
+                    .setOnMenuItemClickListener(this);
+        }
+
+        if (callType == CallLog.Calls.VOICEMAIL_TYPE
+                && phoneCallDetailsViews.voicemailTranscriptionView.length() > 0) {
+            menu.add(ContextMenu.NONE, R.id.context_menu_copy_transcript_to_clipboard,
+                    ContextMenu.NONE, R.string.copy_transcript_text)
+                    .setOnMenuItemClickListener(this);
+        }
+
+        try {
+            mFilteredNumberAsyncQueryHandler.isBlocked(
+                    new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() {
+                        @Override
+                        public void onCheckComplete(Integer id) {
+                            blockId = id;
+                            int blockTitleId = blockId == null ? R.string.call_log_block_number
+                                    : R.string.call_log_unblock_number;
+                            final MenuItem blockItem = menu.add(
+                                    ContextMenu.NONE,
+                                    R.id.context_menu_block_number,
+                                    ContextMenu.NONE,
+                                    blockTitleId);
+                            blockItem.setOnMenuItemClickListener(
+                                    CallLogListItemViewHolder.this);
+                        }
+                    }, info.normalizedNumber, number, countryIso);
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.context_menu_block_number:
+                FilterNumberDialogFragment newFragment =
+                        FilterNumberDialogFragment.newInstance(blockId, info.normalizedNumber,
+                                number, countryIso, info.formattedNumber);
+                newFragment.setQueryHandler(mFilteredNumberAsyncQueryHandler);
+                newFragment.show(((Activity) mContext).getFragmentManager(),
+                        FilterNumberDialogFragment.BLOCK_DIALOG_FRAGMENT);
+                return true;
+            case R.id.context_menu_copy_to_clipboard:
+                ClipboardUtils.copyText(mContext, null, number, true);
+                return true;
+            case R.id.context_menu_copy_transcript_to_clipboard:
+                ClipboardUtils.copyText(mContext, null,
+                        phoneCallDetailsViews.voicemailTranscriptionView.getText(), true);
+                return true;
+            case R.id.context_menu_edit_before_call:
+                final Intent intent = new Intent(
+                        Intent.ACTION_DIAL, CallUtil.getCallUri(number));
+                intent.setClass(mContext, DialtactsActivity.class);
+                DialerUtils.startActivityWithErrorToast(mContext, intent);
+                return true;
+        }
+        return false;
+    }
+
     /**
      * Configures the action buttons in the expandable actions ViewStub. The ViewStub is not
      * inflated during initial binding, so click handlers, tags and accessibility text must be set
@@ -488,6 +601,7 @@
                 telecomCallLogCache,
                 new CallLogListItemHelper(phoneCallDetailsHelper, resources, telecomCallLogCache),
                 null /* voicemailPlaybackPresenter */,
+                null /* filteredNumberAsyncQueryHandler */,
                 new View(context),
                 new QuickContactBadge(context),
                 new View(context),
diff --git a/src/com/android/dialer/calllog/CallLogQueryHandler.java b/src/com/android/dialer/calllog/CallLogQueryHandler.java
index 60bdcff..771df99 100644
--- a/src/com/android/dialer/calllog/CallLogQueryHandler.java
+++ b/src/com/android/dialer/calllog/CallLogQueryHandler.java
@@ -59,6 +59,8 @@
     private static final int UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN = 56;
     /** The token for the query to fetch voicemail status messages. */
     private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 57;
+    /** The token for the query to fetch the number of unread voicemails. */
+    private static final int QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN = 58;
 
     private final int mLogLimit;
 
@@ -147,6 +149,13 @@
         }
     }
 
+    public void fetchVoicemailUnreadCount() {
+        if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
+            startQuery(QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN, null, Voicemails.CONTENT_URI,
+                new String[] { Voicemails._ID }, Voicemails.IS_READ + "=0", null, null);
+        }
+    }
+
     /** Fetches the list of calls in the call log. */
     private void fetchCalls(int token, int callType, boolean newOnly, long newerThan) {
         // We need to check for NULL explicitly otherwise entries with where READ is NULL
@@ -243,6 +252,8 @@
                 }
             } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
                 updateVoicemailStatus(cursor);
+            } else if (token == QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN) {
+                updateVoicemailUnreadCount(cursor);
             } else {
                 Log.w(TAG, "Unknown query completed: ignoring: " + token);
             }
@@ -273,11 +284,21 @@
         }
     }
 
+    private void updateVoicemailUnreadCount(Cursor statusCursor) {
+        final Listener listener = mListener.get();
+        if (listener != null) {
+            listener.onVoicemailUnreadCountFetched(statusCursor);
+        }
+    }
+
     /** Listener to completion of various queries. */
     public interface Listener {
         /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
         void onVoicemailStatusFetched(Cursor statusCursor);
 
+        /** Called when {@link CallLogQueryHandler#fetchVoicemailUnreadCount()} completes. */
+        void onVoicemailUnreadCountFetched(Cursor cursor);
+
         /**
          * Called when {@link CallLogQueryHandler#fetchCalls(int)} complete.
          * Returns true if takes ownership of cursor.
diff --git a/src/com/android/dialer/calllog/GroupingListAdapter.java b/src/com/android/dialer/calllog/GroupingListAdapter.java
index 54dd5f6..70190df 100644
--- a/src/com/android/dialer/calllog/GroupingListAdapter.java
+++ b/src/com/android/dialer/calllog/GroupingListAdapter.java
@@ -126,7 +126,7 @@
      * that position.
      */
     public int getGroupSize(int listPosition) {
-        if (listPosition >= mGroupMetadata.size()) {
+        if (listPosition < 0 || listPosition >= mGroupMetadata.size()) {
             return 0;
         }
 
@@ -138,7 +138,7 @@
      * corresponding to that position.
      */
     public Object getItem(int listPosition) {
-        if (mCursor == null || listPosition >= mGroupMetadata.size()) {
+        if (mCursor == null || listPosition < 0 || listPosition >= mGroupMetadata.size()) {
             return null;
         }
 
diff --git a/src/com/android/dialer/calllog/PromoCardViewHolder.java b/src/com/android/dialer/calllog/PromoCardViewHolder.java
index 656b669..f5a7501 100644
--- a/src/com/android/dialer/calllog/PromoCardViewHolder.java
+++ b/src/com/android/dialer/calllog/PromoCardViewHolder.java
@@ -24,7 +24,8 @@
 import com.android.dialer.R;
 
 /**
- * View holder class for a promo card which will appear in the voicemail tab.
+ * Generic ViewHolder class for a promo card with a primary and secondary action.
+ * Example: the promo card which appears in the voicemail tab.
  */
 public class PromoCardViewHolder extends RecyclerView.ViewHolder {
     public static PromoCardViewHolder create(View rootView) {
@@ -32,14 +33,15 @@
     }
 
     /**
-     * The "Settings" button view.
+     * The primary action button view.
      */
-    private View mSettingsTextView;
+    private View mPrimaryActionView;
 
     /**
+     * The secondary action button view.
      * The "Ok" button view.
      */
-    private View mOkTextView;
+    private View mSecondaryActionView;
 
     /**
      * Creates an instance of the {@link ViewHolder}.
@@ -49,33 +51,33 @@
     private PromoCardViewHolder(View rootView) {
         super(rootView);
 
-        mSettingsTextView = rootView.findViewById(R.id.settings_action);
-        mOkTextView = rootView.findViewById(R.id.ok_action);
+        mPrimaryActionView = rootView.findViewById(R.id.primary_action);
+        mSecondaryActionView = rootView.findViewById(R.id.secondary_action);
     }
 
-    /**
-     * Retrieves the "Settings" button.
+   /**
+     * Retrieves the "primary" action button (eg. "OK").
      *
      * @return The view.
      */
-    public View getSettingsTextView() {
-        return mSettingsTextView;
+    public View getPrimaryActionView() {
+        return mPrimaryActionView;
     }
 
     /**
-     * Retrieves the "Ok" button.
+     * Retrieves the "secondary" action button (eg. "Cancel" or "More Info").
      *
      * @return The view.
      */
-    public View getOkTextView() {
-        return mOkTextView;
+    public View getSecondaryActionView() {
+        return mSecondaryActionView;
     }
 
     @NeededForTesting
     public static PromoCardViewHolder createForTest(Context context) {
         PromoCardViewHolder viewHolder = new PromoCardViewHolder(new View(context));
-        viewHolder.mSettingsTextView = new View(context);
-        viewHolder.mOkTextView = new View(context);
+        viewHolder.mPrimaryActionView = new View(context);
+        viewHolder.mSecondaryActionView = new View(context);
         return viewHolder;
     }
 }
diff --git a/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java b/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java
index 58a717b..2fdea0d 100644
--- a/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java
+++ b/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java
@@ -21,6 +21,8 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
 import android.net.Uri;
 import android.telephony.PhoneNumberUtils;
 
@@ -51,15 +53,28 @@
     }
 
     public interface OnCheckBlockedListener {
-        public void onQueryComplete(Integer id);
+        /**
+         * Invoked after querying if a number is blocked.
+         * @param id The ID of the row if blocked, null otherwise.
+         */
+        public void onCheckComplete(Integer id);
     }
 
     public interface OnBlockNumberListener {
-        public void onInsertComplete(Uri uri);
+        /**
+         * Invoked after inserting a blocked number.
+         * @param uri The uri of the newly created row.
+         */
+        public void onBlockComplete(Uri uri);
     }
 
     public interface OnUnblockNumberListener {
-        public void onDeleteComplete(int rows);
+        /**
+         * Invoked after removing a blocked number
+         * @param rows The number of rows affected (expected value 1).
+         * @param values The deleted data (used for restoration).
+         */
+        public void onUnblockComplete(int rows, ContentValues values);
     }
 
     @Override
@@ -93,7 +108,6 @@
     /**
      * Check if the number + country iso given has been blocked.
      * This method normalizes the number for the lookup if normalizedNumber is null.
-     * Returns to the listener the the ID of the row if blocked, null otherwise.
      */
     public final void isBlocked(final OnCheckBlockedListener listener,
                                 String normalizedNumber, String number, String countryIso) {
@@ -108,25 +122,24 @@
 
     /**
      * Check if the normalized number given has been blocked.
-     * Returns to the listener  the ID of the row if blocked, null otherwise.
      */
     public final void isBlocked(final OnCheckBlockedListener listener,
-                                       String normalizedNumber) {
+                                String normalizedNumber) {
         startQuery(NO_TOKEN,
                 new Listener() {
                     @Override
                     protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
                         if (cursor.getCount() != 1) {
-                            listener.onQueryComplete(null);
+                            listener.onCheckComplete(null);
                             return;
                         }
                         cursor.moveToFirst();
                         if (cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns.TYPE))
                                 != FilteredNumberTypes.BLOCKED_NUMBER) {
-                            listener.onQueryComplete(null);
+                            listener.onCheckComplete(null);
                             return;
                         }
-                        listener.onQueryComplete(
+                        listener.onCheckComplete(
                                 cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)));
                     }
                 },
@@ -139,22 +152,11 @@
 
     /**
      * Add a number manually blocked by the user.
-     * Returns to the listener the URL of the newly created row.
      */
     public final void blockNumber(final OnBlockNumberListener listener,
-                                  String number, String countryIso) {
-        blockNumber(listener,
-                PhoneNumberUtils.formatNumberToE164(number, countryIso), number, countryIso);
-    }
-
-    /**
-     * Add a number manually blocked by the user.
-     * Returns to the listener the URL of the newly created row.
-     */
-    public final void blockNumber(final OnBlockNumberListener listener,
-                                        String normalizedNumber, String number, String countryIso) {
+                                  String normalizedNumber, String number, String countryIso) {
         if (normalizedNumber == null) {
-            blockNumber(listener, number, countryIso);
+            normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
         }
         ContentValues v = new ContentValues();
         v.put(FilteredNumberColumns.NORMALIZED_NUMBER, normalizedNumber);
@@ -162,30 +164,61 @@
         v.put(FilteredNumberColumns.COUNTRY_ISO, countryIso);
         v.put(FilteredNumberColumns.TYPE, FilteredNumberTypes.BLOCKED_NUMBER);
         v.put(FilteredNumberColumns.SOURCE, FilteredNumberSources.USER);
+        blockNumber(listener, v);
+    }
+
+    /**
+     * Block a number with specified ContentValues. Can be manually added or a restored row
+     * from performing the 'undo' action after unblocking.
+     */
+    public final void blockNumber(final OnBlockNumberListener listener, ContentValues values) {
         startInsert(NO_TOKEN,
                 new Listener() {
                     @Override
                     public void onInsertComplete(int token, Object cookie, Uri uri) {
-                        listener.onInsertComplete(uri);
+                        listener.onBlockComplete(uri);
                     }
-                }, getContentUri(null), v);
+                }, getContentUri(null), values);
     }
 
     /**
      * Removes row from database.
      * Caller should call {@link FilteredNumberAsyncQueryHandler#isBlocked} first.
-     * @param id the ID of the row to remove, from {@link FilteredNumberAsyncQueryHandler#isBlocked}.
-     * Returns to the listener the number of rows affected. Expected value is 1.
+     * @param id The ID of row to remove, from {@link FilteredNumberAsyncQueryHandler#isBlocked}.
      */
     public final void unblock(final OnUnblockNumberListener listener, Integer id) {
         if (id == null) {
             throw new IllegalArgumentException("Null id passed into unblock");
         }
-        startDelete(NO_TOKEN, new Listener() {
-            @Override
-            public void onDeleteComplete(int token, Object cookie, int result) {
-                listener.onDeleteComplete(result);
-            }
-        }, getContentUri(id), null, null);
+        unblock(listener, getContentUri(id));
     }
-}
+
+    /**
+     * Removes row from database.
+     * @param uri The uri of row to remove, from
+     *         {@link FilteredNumberAsyncQueryHandler#blockNumber}.
+     */
+    public final void unblock(final OnUnblockNumberListener listener, final Uri uri) {
+        startQuery(NO_TOKEN, new Listener() {
+            @Override
+            public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+                if (cursor.getCount() != 1) {
+                    throw new SQLiteDatabaseCorruptException
+                            ("Returned " + cursor.getCount() + " rows for uri "
+                                    + uri + "where 1 expected.");
+                }
+                cursor.moveToFirst();
+                final ContentValues values = new ContentValues();
+                DatabaseUtils.cursorRowToContentValues(cursor, values);
+                values.remove(FilteredNumberColumns._ID);
+
+                startDelete(NO_TOKEN, new Listener() {
+                    @Override
+                    public void onDeleteComplete(int token, Object cookie, int result) {
+                        listener.onUnblockComplete(result, values);
+                    }
+                }, uri, null, null);
+            }
+        }, uri, null, null, null, null);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java
index 01dc892..0bbf802 100644
--- a/src/com/android/dialer/dialpad/DialpadFragment.java
+++ b/src/com/android/dialer/dialpad/DialpadFragment.java
@@ -481,7 +481,10 @@
      * @param intent The intent.
      * @return {@literal true} if add call operation was requested.  {@literal false} otherwise.
      */
-    private static boolean isAddCallMode(Intent intent) {
+    public static boolean isAddCallMode(Intent intent) {
+        if (intent == null) {
+            return false;
+        }
         final String action = intent.getAction();
         if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
             // see if we are "adding a call" from the InCallScreen; false by default.
diff --git a/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java b/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java
new file mode 100644
index 0000000..f94d0f8
--- /dev/null
+++ b/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2015 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.filterednumber;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.ContentValues;
+import android.content.DialogInterface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.design.widget.Snackbar;
+import android.view.View;
+
+import com.android.dialer.R;
+import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
+
+public class FilterNumberDialogFragment extends DialogFragment {
+    public static final String BLOCK_DIALOG_FRAGMENT = "blockUnblockNumberDialog";
+
+    private static final String ARG_BLOCK_ID = "argBlockId";
+    private static final String ARG_NORMALIZED_NUMBER = "argNormalizedNumber";
+    private static final String ARG_NUMBER = "argNumber";
+    private static final String ARG_COUNTRY_ISO = "argCountryIso";
+    private static final String ARG_DISPLAY_NUMBER = "argDisplayNumber";
+
+    private FilteredNumberAsyncQueryHandler mHandler;
+
+    public void setQueryHandler (FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) {
+        mHandler = filteredNumberAsyncQueryHandler;
+    }
+
+    public static FilterNumberDialogFragment newInstance(Integer blockId, String normalizedNumber,
+        String number, String countryIso, String displayNumber) {
+        final FilterNumberDialogFragment fragment = new FilterNumberDialogFragment();
+        final Bundle args = new Bundle();
+        if (blockId != null) {
+            args.putInt(ARG_BLOCK_ID, blockId.intValue());
+        }
+        args.putString(ARG_NORMALIZED_NUMBER, normalizedNumber);
+        args.putString(ARG_NUMBER, number);
+        args.putString(ARG_COUNTRY_ISO, countryIso);
+        args.putString(ARG_DISPLAY_NUMBER, displayNumber);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        super.onCreateDialog(savedInstanceState);
+        final boolean isBlocked = getArguments().containsKey(ARG_BLOCK_ID);
+        final String displayNumber = getArguments().getString(ARG_DISPLAY_NUMBER);
+
+        String message;
+        String okText;
+        if (isBlocked) {
+            message = getString(R.string.unblockNumberConfirmation, displayNumber);
+            okText = getString(R.string.unblockNumberOk);
+        } else {
+            message = getString(R.string.blockNumberConfirmation, displayNumber);
+            okText = getString(R.string.blockNumberOk);
+        }
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+                .setMessage(message)
+                .setPositiveButton(okText, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int id) {
+                        if (isBlocked) {
+                            unblockNumber();
+                        } else {
+                            blockNumber();
+                        }
+                    }
+                })
+                .setNegativeButton(android.R.string.cancel, null);
+        return builder.create();
+    }
+
+    public void blockNumber() {
+        final View view = getActivity().findViewById(R.id.floating_action_button_container);
+        final String displayNumber = getArguments().getString(ARG_DISPLAY_NUMBER);
+        final String message = getString(R.string.snackbar_number_blocked, displayNumber);
+        final String undoMessage = getString(R.string.snackbar_number_unblocked, displayNumber);
+        final FilteredNumberAsyncQueryHandler.OnUnblockNumberListener undoListener =
+                new FilteredNumberAsyncQueryHandler.OnUnblockNumberListener() {
+                    @Override
+                    public void onUnblockComplete(int rows, ContentValues values) {
+                        Snackbar.make(view, undoMessage, Snackbar.LENGTH_LONG).show();
+                    }
+                };
+
+        mHandler.blockNumber(
+                new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() {
+                    @Override
+                    public void onBlockComplete(final Uri uri) {
+                        Snackbar.make(view, message, Snackbar.LENGTH_LONG)
+                                .setAction(R.string.block_number_undo,
+                                        // Delete the newly created row on 'undo'.
+                                        new View.OnClickListener() {
+                                            @Override
+                                            public void onClick(View view) {
+                                                mHandler.unblock(undoListener, uri);
+                                            }
+                                        })
+                                .show();
+                    }
+                }, getArguments().getString(ARG_NORMALIZED_NUMBER),
+                getArguments().getString(ARG_NUMBER), getArguments().getString(ARG_COUNTRY_ISO));
+    }
+
+    public void unblockNumber() {
+        final View view = getActivity().findViewById(R.id.floating_action_button_container);
+        final String displayNumber = getArguments().getString(ARG_DISPLAY_NUMBER);
+        final String message = getString(R.string.snackbar_number_unblocked, displayNumber);
+        final String undoMessage = getString(R.string.snackbar_number_blocked, displayNumber);
+        final FilteredNumberAsyncQueryHandler.OnBlockNumberListener undoListener =
+                new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() {
+                    @Override
+                    public void onBlockComplete(final Uri uri) {
+                        Snackbar.make(view, undoMessage, Snackbar.LENGTH_LONG).show();
+                    }
+                };
+        mHandler.unblock(
+                new FilteredNumberAsyncQueryHandler.OnUnblockNumberListener() {
+                    @Override
+                    public void onUnblockComplete(int rows, final ContentValues values) {
+                        Snackbar.make(view, message, Snackbar.LENGTH_LONG)
+                                .setAction(R.string.block_number_undo,
+                                        new View.OnClickListener() {
+                                            // Re-insert the row on 'undo', with a new ID.
+                                            @Override
+                                            public void onClick(View view) {
+                                                mHandler.blockNumber(undoListener, values);
+                                            }
+                                        })
+                                .show();
+                    }
+                }, getArguments().getInt(ARG_BLOCK_ID));
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/dialer/list/ListsFragment.java b/src/com/android/dialer/list/ListsFragment.java
index feab201..09a4cb2 100644
--- a/src/com/android/dialer/list/ListsFragment.java
+++ b/src/com/android/dialer/list/ListsFragment.java
@@ -110,6 +110,7 @@
      * The position of the currently selected tab.
      */
     private int mTabIndex = TAB_INDEX_SPEED_DIAL;
+    private CallLogQueryHandler mCallLogQueryHandler;
 
     public class ViewPagerAdapter extends FragmentPagerAdapter {
         public ViewPagerAdapter(FragmentManager fm) {
@@ -199,9 +200,9 @@
         }
 
         // Fetch voicemail status to determine if we should show the voicemail tab.
-        CallLogQueryHandler callLogQueryHandler =
+        mCallLogQueryHandler =
                 new CallLogQueryHandler(getActivity(), getActivity().getContentResolver(), this);
-        callLogQueryHandler.fetchVoicemailStatus();
+        mCallLogQueryHandler.fetchVoicemailStatus();
         Trace.endSection();
     }
 
@@ -227,13 +228,13 @@
         mTabTitles[TAB_INDEX_VOICEMAIL] = getResources().getString(R.string.tab_voicemail);
 
         mTabIcons = new int[TAB_COUNT_WITH_VOICEMAIL];
-        mTabIcons[TAB_INDEX_SPEED_DIAL] = R.drawable.tab_speed_dial;
-        mTabIcons[TAB_INDEX_HISTORY] = R.drawable.tab_history;
-        mTabIcons[TAB_INDEX_ALL_CONTACTS] = R.drawable.tab_contacts;
-        mTabIcons[TAB_INDEX_VOICEMAIL] = R.drawable.tab_voicemail;
+        mTabIcons[TAB_INDEX_SPEED_DIAL] = R.drawable.ic_grade_24dp;
+        mTabIcons[TAB_INDEX_HISTORY] = R.drawable.ic_schedule_24dp;
+        mTabIcons[TAB_INDEX_ALL_CONTACTS] = R.drawable.ic_people_24dp;
+        mTabIcons[TAB_INDEX_VOICEMAIL] = R.drawable.ic_voicemail_24dp;
 
         mViewPagerTabs = (ViewPagerTabs) parentView.findViewById(R.id.lists_pager_header);
-        mViewPagerTabs.setTabIcons(mTabIcons);
+        mViewPagerTabs.configureTabIcons(mTabIcons);
         mViewPagerTabs.setViewPager(mViewPager);
         addOnPageChangeListener(mViewPagerTabs);
 
@@ -319,8 +320,12 @@
             mViewPagerTabs.setViewPager(mViewPager);
 
             mPrefs.edit()
-                    .putBoolean(PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, hasActiveVoicemailProvider)
-                    .commit();
+                  .putBoolean(PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, hasActiveVoicemailProvider)
+                  .commit();
+        }
+
+        if (hasActiveVoicemailProvider) {
+            mCallLogQueryHandler.fetchVoicemailUnreadCount();
         }
 
         if (mHasActiveVoicemailProvider && mShowVoicemailTabAfterVoicemailStatusIsFetched) {
@@ -330,6 +335,23 @@
     }
 
     @Override
+    public void onVoicemailUnreadCountFetched(Cursor cursor) {
+        if (getActivity() == null || getActivity().isFinishing() || cursor == null) {
+            return;
+        }
+
+        int count = 0;
+        try {
+            count = cursor.getCount();
+        } finally {
+            cursor.close();
+        }
+
+        mViewPagerTabs.setUnreadCount(count, TAB_INDEX_VOICEMAIL);
+        mViewPagerTabs.setViewPager(mViewPager);
+    }
+
+    @Override
     public boolean onCallsFetched(Cursor statusCursor) {
         // Return false; did not take ownership of cursor
         return false;
@@ -339,6 +361,16 @@
         return mTabIndex;
     }
 
+    /**
+     * External method to update unread count because the unread count changes when the user
+     * expands a voicemail in the call log.
+     */
+    public void updateTabUnreadCounts() {
+        if (mHasActiveVoicemailProvider) {
+            mCallLogQueryHandler.fetchVoicemailUnreadCount();
+        }
+    }
+
     public void showRemoveView(boolean show) {
         mRemoveViewContent.setVisibility(show ? View.VISIBLE : View.GONE);
         mRemoveView.setAlpha(show ? 0 : 1);
diff --git a/tests/src/com/android/dialer/CallDetailActivityTest.java b/tests/src/com/android/dialer/CallDetailActivityTest.java
index c874244..59c2434 100644
--- a/tests/src/com/android/dialer/CallDetailActivityTest.java
+++ b/tests/src/com/android/dialer/CallDetailActivityTest.java
@@ -29,12 +29,12 @@
 import android.test.suitebuilder.annotation.LargeTest;
 import android.test.suitebuilder.annotation.Suppress;
 import android.view.Menu;
+import android.widget.PopupMenu;
 import android.widget.TextView;
 
 import com.android.dialer.calllog.CallLogAsyncTaskUtil;
 import com.android.dialer.util.AsyncTaskExecutors;
 import com.android.dialer.util.FakeAsyncTaskExecutor;
-// import com.android.internal.view.menu.ContextMenuBuilder;
 
 /**
  * Unit tests for the {@link CallDetailActivity}. NOTE: The screen needs to be on for the
@@ -86,33 +86,33 @@
      * Test for bug where voicemails should not have remove-from-call-log entry.
      * <p>
      * See http://b/5054103.
+     */
     public void testVoicemailDoesNotHaveRemoveFromCallLog() throws Throwable {
         setActivityIntentForTestVoicemailEntry();
         startActivityUnderTest();
         mFakeAsyncTaskExecutor.runTask(GET_CALL_DETAILS);
 
-        Menu menu = new ContextMenuBuilder(mActivityUnderTest);
-        mActivityUnderTest.onCreateOptionsMenu(menu);
-        mActivityUnderTest.onPrepareOptionsMenu(menu);
-        assertFalse(menu.findItem(R.id.menu_remove_from_call_log).isVisible());
-        assertTrue(menu.findItem(R.id.menu_trash).isVisible());
+        Menu optionsMenu = (new PopupMenu(mActivityUnderTest, null)).getMenu();
+        mActivityUnderTest.onCreateOptionsMenu(optionsMenu);
+        mActivityUnderTest.onPrepareOptionsMenu(optionsMenu);
+        assertFalse(optionsMenu.findItem(R.id.menu_remove_from_call_log).isVisible());
+        assertTrue(optionsMenu.findItem(R.id.menu_trash).isVisible());
     }
-    */
 
     /**
      * Test to check that I haven't broken the remove-from-call-log entry from regular calls.
+     */
     public void testRegularCallDoesHaveRemoveFromCallLog() throws Throwable {
         setActivityIntentForTestCallEntry();
         startActivityUnderTest();
         mFakeAsyncTaskExecutor.runTask(GET_CALL_DETAILS);
 
-        Menu menu = new ContextMenuBuilder(mActivityUnderTest);
-        mActivityUnderTest.onCreateOptionsMenu(menu);
-        mActivityUnderTest.onPrepareOptionsMenu(menu);
-        assertTrue(menu.findItem(R.id.menu_remove_from_call_log).isVisible());
-        assertFalse(menu.findItem(R.id.menu_trash).isVisible());
+        Menu optionsMenu = (new PopupMenu(mActivityUnderTest, null)).getMenu();
+        mActivityUnderTest.onCreateOptionsMenu(optionsMenu);
+        mActivityUnderTest.onPrepareOptionsMenu(optionsMenu);
+        assertTrue(optionsMenu.findItem(R.id.menu_remove_from_call_log).isVisible());
+        assertFalse(optionsMenu.findItem(R.id.menu_trash).isVisible());
     }
-    */
 
     private void setActivityIntentForTestCallEntry() {
         assertNull(mVoicemailUri);