Merge "Fix for inconsistent smart dial database" into ub-contactsdialer-a-dev
diff --git a/res/drawable-hdpi/ic_mic_grey600.png b/res/drawable-hdpi/ic_mic_grey600.png
new file mode 100644
index 0000000..4b67cf7
--- /dev/null
+++ b/res/drawable-hdpi/ic_mic_grey600.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_voice_search.png b/res/drawable-hdpi/ic_voice_search.png
deleted file mode 100644
index 9631d3e..0000000
--- a/res/drawable-hdpi/ic_voice_search.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_mic_grey600.png b/res/drawable-mdpi/ic_mic_grey600.png
new file mode 100644
index 0000000..2310c73
--- /dev/null
+++ b/res/drawable-mdpi/ic_mic_grey600.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_voice_search.png b/res/drawable-mdpi/ic_voice_search.png
deleted file mode 100644
index af58538..0000000
--- a/res/drawable-mdpi/ic_voice_search.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_mic_grey600.png b/res/drawable-xhdpi/ic_mic_grey600.png
new file mode 100644
index 0000000..a9a83b3
--- /dev/null
+++ b/res/drawable-xhdpi/ic_mic_grey600.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_voice_search.png b/res/drawable-xhdpi/ic_voice_search.png
deleted file mode 100644
index 6e5d811..0000000
--- a/res/drawable-xhdpi/ic_voice_search.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_mic_grey600.png b/res/drawable-xxhdpi/ic_mic_grey600.png
new file mode 100644
index 0000000..07128dd
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_mic_grey600.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_voice_search.png b/res/drawable-xxhdpi/ic_voice_search.png
deleted file mode 100644
index 4e72f69..0000000
--- a/res/drawable-xxhdpi/ic_voice_search.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_mic_grey600.png b/res/drawable-xxxhdpi/ic_mic_grey600.png
new file mode 100644
index 0000000..b7403ff
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_mic_grey600.png
Binary files differ
diff --git a/res/drawable/ic_voicemail_seek_handle_disabled.xml b/res/drawable/ic_voicemail_seek_handle_disabled.xml
new file mode 100644
index 0000000..1262808
--- /dev/null
+++ b/res/drawable/ic_voicemail_seek_handle_disabled.xml
@@ -0,0 +1,21 @@
+<?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
+  -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_handle"
+        android:autoMirrored="true"
+        android:tint="@color/voicemail_icon_disabled_tint" >
+</bitmap>
\ No newline at end of file
diff --git a/res/layout/blocked_number_fragment.xml b/res/layout/blocked_number_fragment.xml
new file mode 100644
index 0000000..b1e4d0f
--- /dev/null
+++ b/res/layout/blocked_number_fragment.xml
@@ -0,0 +1,56 @@
+<?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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:card_view="http://schemas.android.com/apk/res-auto"
+    android:orientation="vertical"
+    android:background="@color/blocked_number_background"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <android.support.v7.widget.CardView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        card_view:cardCornerRadius="0dp">
+
+        <ListView android:id="@id/android:list"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@color/background_dialer_white"
+            android:layout_weight="1"
+            android:drawSelectorOnTop="false"
+            android:headerDividersEnabled="false" />
+
+        <LinearLayout
+            android:id="@android:id/empty"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
+            android:orientation="vertical">
+
+            <include layout="@layout/blocked_number_header" />
+
+            <TextView android:id="@id/android:empty"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:paddingStart="@dimen/blocked_number_horizontal_margin"
+                android:paddingTop="@dimen/blocked_number_top_margin"
+                android:paddingBottom="@dimen/blocked_number_bottom_margin"
+                android:text="@string/listNoBlockedNumbers" />
+
+        </LinearLayout>
+
+    </android.support.v7.widget.CardView>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/blocked_number_header.xml b/res/layout/blocked_number_header.xml
new file mode 100644
index 0000000..fed94cc
--- /dev/null
+++ b/res/layout/blocked_number_header.xml
@@ -0,0 +1,40 @@
+<?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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical" android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <TextView
+        android:id="@+id/textView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/blockList"
+        android:paddingStart="@dimen/blocked_number_horizontal_margin"
+        android:paddingTop="@dimen/blocked_number_top_margin"
+        android:paddingBottom="@dimen/blocked_number_bottom_margin"
+        android:textColor="@color/blocked_number_accent_color"
+        style="@android:style/TextAppearance.Material.Subhead" />
+
+    <Button
+        android:id="@+id/add_number_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/blockNumber"
+        android:layout_gravity="right"
+        android:textColor="@color/blocked_number_accent_color"
+        style="?android:attr/borderlessButtonStyle" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/blocked_number_item.xml b/res/layout/blocked_number_item.xml
new file mode 100644
index 0000000..6c87533
--- /dev/null
+++ b/res/layout/blocked_number_item.xml
@@ -0,0 +1,75 @@
+<?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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/caller_information"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingStart="@dimen/blocked_number_horizontal_margin"
+    android:paddingTop="@dimen/blocked_number_top_margin"
+    android:paddingBottom="@dimen/blocked_number_bottom_margin"
+    android:baselineAligned="false"
+    android:gravity="center_vertical"
+    android:orientation="horizontal"
+    android:focusable="true"
+    android:background="@color/background_dialer_white">
+
+    <QuickContactBadge
+        android:id="@+id/quick_contact_photo"
+        android:layout_width="@dimen/contact_photo_size"
+        android:layout_height="@dimen/contact_photo_size"
+        android:focusable="true" />
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="vertical"
+        android:gravity="center_vertical"
+        android:layout_marginStart="@dimen/blocked_number_horizontal_margin">
+
+        <TextView
+            android:id="@+id/caller_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@color/blocked_number_primary_text_color"
+            android:textSize="@dimen/blocked_number_primary_text_size"
+            android:includeFontPadding="false"
+            android:layout_marginBottom="5dp"
+            android:singleLine="true" />
+
+        <TextView
+            android:id="@+id/caller_number"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@color/blocked_number_secondary_text_color"
+            android:textSize="@dimen/blocked_number_secondary_text_size"
+            android:layout_marginBottom="1dp"
+            android:singleLine="true" />
+    </LinearLayout>
+
+    <ImageView
+        android:id="@+id/delete_button"
+        android:layout_width="@dimen/blocked_number_delete_icon_size"
+        android:layout_height="@dimen/blocked_number_delete_icon_size"
+        android:layout_marginEnd="16dp"
+        android:background="?android:attr/selectableItemBackgroundBorderless"
+        android:src="@drawable/ic_remove"
+        android:scaleType="center"
+        android:tint="@color/delete_icon_tint"
+        android:contentDescription="@string/description_blocked_number_list_delete" />
+
+</LinearLayout>
diff --git a/res/layout/search_edittext.xml b/res/layout/search_edittext.xml
index 61406a1..8eda696 100644
--- a/res/layout/search_edittext.xml
+++ b/res/layout/search_edittext.xml
@@ -17,7 +17,6 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:paddingStart="@dimen/search_box_left_padding"
-        android:paddingEnd="@dimen/search_box_right_padding"
         android:gravity="center_vertical"
         android:orientation="horizontal">
 
@@ -25,8 +24,8 @@
             android:id="@+id/search_magnifying_glass"
             android:layout_height="@dimen/search_box_icon_size"
             android:layout_width="@dimen/search_box_icon_size"
-            android:layout_margin="@dimen/search_box_icon_margin"
             android:padding="@dimen/search_box_search_icon_padding"
+            android:scaleType="center"
             android:src="@drawable/ic_ab_search"
             android:importantForAccessibility="no"
             android:tint="@color/searchbox_icon_tint" />
@@ -45,10 +44,10 @@
 
         <ImageView
             android:id="@+id/voice_search_button"
-            android:layout_height="@dimen/search_box_icon_size"
             android:layout_width="@dimen/search_box_icon_size"
-            android:layout_margin="@dimen/search_box_icon_margin"
-            android:src="@drawable/ic_voice_search"
+            android:layout_height="match_parent"
+            android:src="@drawable/ic_mic_grey600"
+            android:scaleType="center"
             android:clickable="true"
             android:contentDescription="@string/description_start_voice_search"
             android:background="?android:attr/selectableItemBackgroundBorderless"
@@ -57,8 +56,9 @@
         <ImageButton
             android:id="@+id/dialtacts_options_menu_button"
             android:layout_width="@dimen/search_box_icon_size"
-            android:layout_height="@dimen/search_box_icon_size"
-            android:layout_margin="@dimen/search_box_icon_margin"
+            android:layout_height="match_parent"
+            android:paddingEnd="@dimen/search_box_right_padding"
+            android:scaleType="center"
             android:background="?android:attr/selectableItemBackgroundBorderless"
             android:src="@drawable/ic_overflow_menu"
             android:contentDescription="@string/action_menu_overflow_description"
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 8ce3c17..a747927 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -104,4 +104,11 @@
     <color name="floating_action_button_touch_tint">#80ffffff</color>
 
     <color name="call_log_action_divider">#eeeeee</color>
+
+    <!--  Colors for blocked numbers list -->
+    <color name="blocked_number_primary_text_color">@color/dialtacts_primary_text_color</color>
+    <color name="blocked_number_secondary_text_color">@color/dialtacts_secondary_text_color</color>
+    <color name="delete_icon_tint">#6D6D6D</color>
+    <color name="blocked_number_background">#E0E0E0</color>
+    <color name="blocked_number_accent_color">#42A5F5</color>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 776cd11..f71f128 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -96,8 +96,6 @@
     <dimen name="search_box_left_padding">8dp</dimen>
     <!-- Search box interior padding - right -->
     <dimen name="search_box_right_padding">8dp</dimen>
-    <!-- Padding around the icon in the search box. -->
-    <dimen name="search_box_icon_margin">4dp</dimen>
     <dimen name="search_box_search_icon_padding">2dp</dimen>
     <dimen name="search_box_collapsed_text_margin_left">22dp</dimen>
     <dimen name="search_list_padding_top">16dp</dimen>
@@ -147,4 +145,12 @@
     <dimen name="promo_card_line_spacing">4dp</dimen>
 
     <dimen name="voicemail_playback_top_padding">12dp</dimen>
+
+    <!-- Size of entries in blocked numbers list -->
+    <dimen name="blocked_number_horizontal_margin">16dp</dimen>
+    <dimen name="blocked_number_top_margin">16dp</dimen>
+    <dimen name="blocked_number_bottom_margin">16dp</dimen>
+    <dimen name="blocked_number_primary_text_size">16sp</dimen>
+    <dimen name="blocked_number_secondary_text_size">12sp</dimen>
+    <dimen name="blocked_number_delete_icon_size">32dp</dimen>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7bd88ba..093aa19 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -805,6 +805,40 @@
     <!-- Label for the call settings section [CHAR LIMIT=30] -->
     <string name="call_settings_label">Calls</string>
 
+    <!-- Label for the blocked calls settings section [CHAR LIMIT=30] -->
+    <string name="blocked_calls_settings_label">Spam and blocked calls</string>
+
+    <!-- String describing the delete icon on a blocked number list item.
+        When tapped, it will show a dialog confirming the unblocking of the number.
+        [CHAR LIMIT=NONE]-->
+    <string name="description_blocked_number_list_delete">Unblock number</string>
+
+    <!-- Displayed in the blocked numbers list when there are no blocked numbers.
+        [CHAR LIMIT=NONE] -->
+    <string name="listNoBlockedNumbers">No blocked numbers</string>
+
+    <!-- Button to bring up UI to add a number to the blocked call list. [CHAR LIMIT=40] -->
+    <string name="blockNumber">Add number</string>
+
+    <!-- Heading for the block list in the "Spam and blocked calls" settings. [CHAR LIMIT=64] -->
+    <string name="blockList">Block list</string>
+
+    <!-- Label for progress dialog when validating a number to be added to the block list.
+        [CHAR LIMIT=64] -->
+    <string name="checkingNumber">Checking
+        <xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g>
+    </string>
+
+    <!-- Error message shown when user tries to add invalid number to the block list.
+        [CHAR LIMIT=64] -->
+    <string name="invalidNumber"><xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g>
+        is invalid.</string>
+
+    <!-- Error message shown when user tries to add a number to the block list that was already
+        blocked. [CHAR LIMIT=64] -->
+    <string name="alreadyBlocked"><xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g>
+        is already blocked.</string>
+
     <!-- Label for the phone account settings [CHAR LIMIT=30] -->
     <string name="phone_account_settings_label">Calling accounts</string>
 
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java
index 72a5012..d95b55e 100644
--- a/src/com/android/dialer/CallDetailActivity.java
+++ b/src/com/android/dialer/CallDetailActivity.java
@@ -31,6 +31,7 @@
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
+import android.view.MotionEvent;
 import android.view.View;
 import android.widget.ListView;
 import android.widget.QuickContactBadge;
@@ -39,6 +40,7 @@
 
 import com.android.contacts.common.ContactPhotoManager;
 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.interactions.TouchPointManager;
 import com.android.contacts.common.GeoUtil;
 import com.android.contacts.common.CallUtil;
 import com.android.contacts.common.util.UriUtils;
@@ -262,6 +264,14 @@
         getCallDetails();
     }
 
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
+        }
+        return super.dispatchTouchEvent(ev);
+    }
+
     public void getCallDetails() {
         CallLogAsyncTaskUtil.getCallDetails(this, getCallLogEntryUris(), mCallLogAsyncTaskListener);
     }
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index dbff276..f8fb17f 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -83,6 +83,7 @@
 import com.android.dialer.list.SpeedDialFragment;
 import com.android.dialer.settings.DialerSettingsActivity;
 import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.TelecomUtil;
 import com.android.dialer.util.IntentUtil.CallIntentBuilder;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.widget.ActionBarController;
@@ -158,6 +159,8 @@
      */
     private SmartDialSearchFragment mSmartDialSearchFragment;
 
+    private boolean mIsVisible;
+
     /**
      * Animation that slides in.
      */
@@ -563,6 +566,25 @@
     }
 
     @Override
+    protected void onStart() {
+        super.onStart();
+        mIsVisible = true;
+    }
+
+    @Override
+    protected void onStop() {
+        mIsVisible = false;
+        super.onStop();
+    }
+
+    /**
+     * Returns true when the Activity is currently visible (between onStart and onStop).
+     */
+    /* package */ boolean isVisible() {
+        return mIsVisible;
+    }
+
+    @Override
     protected void onPause() {
         if (mClearSearchOnPause) {
             hideDialpadAndSearchUi();
@@ -576,6 +598,7 @@
 
     @Override
     protected void onSaveInstanceState(Bundle outState) {
+        mIsVisible = false;
         super.onSaveInstanceState(outState);
         outState.putString(KEY_SEARCH_QUERY, mSearchQuery);
         outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch);
@@ -648,6 +671,10 @@
 
     @Override
     public boolean onMenuItemClick(MenuItem item) {
+        if (!isVisible()) {
+            return true;
+        }
+
         switch (item.getItemId()) {
             case R.id.menu_history:
                 // Use explicit CallLogActivity intent instead of ACTION_VIEW +
@@ -1182,7 +1209,7 @@
     }
 
     private boolean phoneIsInUse() {
-        return getTelecomManager().isInCall();
+        return TelecomUtil.isInCall(this);
     }
 
     private boolean canIntentBeHandled(Intent intent) {
diff --git a/src/com/android/dialer/calllog/CallLogActivity.java b/src/com/android/dialer/calllog/CallLogActivity.java
index ff0726d..ad795f9 100644
--- a/src/com/android/dialer/calllog/CallLogActivity.java
+++ b/src/com/android/dialer/calllog/CallLogActivity.java
@@ -48,6 +48,8 @@
     private CallLogFragment mAllCallsFragment;
     private CallLogFragment mMissedCallsFragment;
 
+    private boolean mIsVisible;
+
     private String[] mTabTitles;
 
     private static final int TAB_INDEX_ALL = 0;
@@ -161,6 +163,31 @@
     }
 
     @Override
+    protected void onStart() {
+        super.onStart();
+        mIsVisible = false;
+    }
+
+    @Override
+    protected void onStop() {
+        mIsVisible = false;
+        super.onStop();
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        mIsVisible = false;
+        super.onSaveInstanceState(outState);
+    }
+
+    /**
+     * Returns true when the Activity is currently visible (between onStart and onStop).
+     */
+    /* package */ boolean isVisible() {
+        return mIsVisible;
+    }
+
+    @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         final MenuInflater inflater = getMenuInflater();
         inflater.inflate(R.menu.call_log_options, menu);
@@ -180,6 +207,10 @@
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
+        if (!isVisible()) {
+            return true;
+        }
+
         switch (item.getItemId()) {
             case android.R.id.home:
                 final Intent intent = new Intent(this, DialtactsActivity.class);
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 26e3965..b7f068e 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -362,6 +362,10 @@
         mAdapter.startCache();
 
         rescheduleDisplayUpdate();
+
+        if (mVoicemailPlaybackPresenter != null) {
+            mVoicemailPlaybackPresenter.onResume();
+        }
     }
 
     @Override
diff --git a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
index 521b2a4..8b2eb60 100644
--- a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
+++ b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
@@ -291,25 +291,22 @@
                     .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) {
-        }
+        mFilteredNumberAsyncQueryHandler.startBlockedQuery(
+                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);
     }
 
     @Override
@@ -320,6 +317,8 @@
                         FilterNumberDialogFragment.newInstance(blockId, info.normalizedNumber,
                                 number, countryIso, info.formattedNumber);
                 newFragment.setQueryHandler(mFilteredNumberAsyncQueryHandler);
+                newFragment.setParentView(
+                        ((Activity) mContext).findViewById(R.id.floating_action_button_container));
                 newFragment.show(((Activity) mContext).getFragmentManager(),
                         FilterNumberDialogFragment.BLOCK_DIALOG_FRAGMENT);
                 return true;
diff --git a/src/com/android/dialer/database/DialerDatabaseHelper.java b/src/com/android/dialer/database/DialerDatabaseHelper.java
index 413e867..cc8631d 100644
--- a/src/com/android/dialer/database/DialerDatabaseHelper.java
+++ b/src/com/android/dialer/database/DialerDatabaseHelper.java
@@ -75,7 +75,7 @@
      *   0-98   KitKat
      * </pre>
      */
-    public static final int DATABASE_VERSION = 6;
+    public static final int DATABASE_VERSION = 7;
     public static final String DATABASE_NAME = "dialer.db";
 
     /**
@@ -457,7 +457,7 @@
             return;
         }
 
-        if (oldVersion < 6) {
+        if (oldVersion < 7) {
             db.execSQL("DROP TABLE IF EXISTS " + Tables.FILTERED_NUMBER_TABLE);
             db.execSQL("CREATE TABLE " + Tables.FILTERED_NUMBER_TABLE + " ("
                     + FilteredNumberColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
@@ -470,7 +470,7 @@
                     + FilteredNumberColumns.TYPE + " INTEGER,"
                     + FilteredNumberColumns.SOURCE + " INTEGER"
                     + ");");
-            oldVersion = 6;
+            oldVersion = 7;
         }
 
         if (oldVersion != DATABASE_VERSION) {
diff --git a/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java b/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java
index 2fdea0d..9da9cc1 100644
--- a/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java
+++ b/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java
@@ -26,6 +26,7 @@
 import android.net.Uri;
 import android.telephony.PhoneNumberUtils;
 
+import com.android.contacts.common.util.PhoneNumberHelper;
 import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
 import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
 import com.android.dialer.database.FilteredNumberContract.FilteredNumberSources;
@@ -79,22 +80,30 @@
 
     @Override
     protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
-        ((Listener) cookie).onQueryComplete(token, cookie, cursor);
+        if (cookie != null) {
+            ((Listener) cookie).onQueryComplete(token, cookie, cursor);
+        }
     }
 
     @Override
     protected void onInsertComplete(int token, Object cookie, Uri uri) {
-        ((Listener) cookie).onInsertComplete(token, cookie, uri);
+        if (cookie != null) {
+            ((Listener) cookie).onInsertComplete(token, cookie, uri);
+        }
     }
 
     @Override
     protected void onUpdateComplete(int token, Object cookie, int result) {
-        ((Listener) cookie).onUpdateComplete(token, cookie, result);
+        if (cookie != null) {
+            ((Listener) cookie).onUpdateComplete(token, cookie, result);
+        }
     }
 
     @Override
     protected void onDeleteComplete(int token, Object cookie, int result) {
-        ((Listener) cookie).onDeleteComplete(token, cookie, result);
+        if (cookie != null) {
+            ((Listener) cookie).onDeleteComplete(token, cookie, result);
+        }
     }
 
     private static Uri getContentUri(Integer id) {
@@ -105,26 +114,43 @@
         return uri;
     }
 
+    public final void incrementFilteredCount(Integer id) {
+        startUpdate(NO_TOKEN, null,
+                ContentUris.withAppendedId(FilteredNumber.CONTENT_URI_INCREMENT_FILTERED_COUNT, id),
+                null, null, null);
+    }
+
     /**
      * Check if the number + country iso given has been blocked.
      * This method normalizes the number for the lookup if normalizedNumber is null.
+     * @return {@code true} if the number was invalid and couldn't be checked,
+     * {@code false} otherwise,
      */
-    public final void isBlocked(final OnCheckBlockedListener listener,
-                                String normalizedNumber, String number, String countryIso) {
+    public final boolean startBlockedQuery(final OnCheckBlockedListener listener,
+                                        String normalizedNumber, String number, String countryIso) {
         if (normalizedNumber == null) {
-            normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+            normalizedNumber = getNormalizedNumber(number, countryIso);
             if (normalizedNumber == null) {
-                throw new IllegalArgumentException("Invalid phone number");
+                return true;
             }
         }
-        isBlocked(listener, normalizedNumber);
+        startBlockedQuery(listener, normalizedNumber);
+        return false;
+    }
+
+    public static String getNormalizedNumber(String number, String countryIso) {
+        if (PhoneNumberHelper.isUriNumber(number)) {
+            return number;
+        } else {
+            return PhoneNumberUtils.formatNumberToE164(number, countryIso);
+        }
     }
 
     /**
      * Check if the normalized number given has been blocked.
      */
-    public final void isBlocked(final OnCheckBlockedListener listener,
-                                String normalizedNumber) {
+    public final void startBlockedQuery(final OnCheckBlockedListener listener,
+                                        String normalizedNumber) {
         startQuery(NO_TOKEN,
                 new Listener() {
                     @Override
@@ -156,7 +182,7 @@
     public final void blockNumber(final OnBlockNumberListener listener,
                                   String normalizedNumber, String number, String countryIso) {
         if (normalizedNumber == null) {
-            normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+            normalizedNumber = getNormalizedNumber(number, countryIso);
         }
         ContentValues v = new ContentValues();
         v.put(FilteredNumberColumns.NORMALIZED_NUMBER, normalizedNumber);
@@ -183,8 +209,8 @@
 
     /**
      * Removes row from database.
-     * Caller should call {@link FilteredNumberAsyncQueryHandler#isBlocked} first.
-     * @param id The ID of row to remove, from {@link FilteredNumberAsyncQueryHandler#isBlocked}.
+     * Caller should call {@link FilteredNumberAsyncQueryHandler#startBlockedQuery} first.
+     * @param id The ID of row to remove, from {@link FilteredNumberAsyncQueryHandler#startBlockedQuery}.
      */
     public final void unblock(final OnUnblockNumberListener listener, Integer id) {
         if (id == null) {
diff --git a/src/com/android/dialer/database/FilteredNumberContract.java b/src/com/android/dialer/database/FilteredNumberContract.java
index 0ec171b..f396681 100644
--- a/src/com/android/dialer/database/FilteredNumberContract.java
+++ b/src/com/android/dialer/database/FilteredNumberContract.java
@@ -16,13 +16,8 @@
 
 package com.android.dialer.database;
 
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.database.Cursor;
 import android.net.Uri;
 import android.provider.BaseColumns;
-import android.telephony.PhoneNumberUtils;
 
 import com.android.dialerbind.ObjectFactory;
 
@@ -61,7 +56,7 @@
 
     public interface FilteredNumberColumns {
         // TYPE: INTEGER
-        static final String _ID = "id";
+        static final String _ID = "_id";
         /**
          * Represents the number to be filtered, normalized to compare phone numbers for equality.
          *
@@ -137,11 +132,17 @@
     public static class FilteredNumber implements BaseColumns {
 
         public static final String FILTERED_NUMBERS_TABLE = "filtered_numbers_table";
+        public static final String FILTERED_NUMBERS_INCREMENT_FILTERED_COUNT =
+                "filtered_numbers_increment_filtered_count";
 
         public static final Uri CONTENT_URI = Uri.withAppendedPath(
                 AUTHORITY_URI,
                 FILTERED_NUMBERS_TABLE);
 
+        public static final Uri CONTENT_URI_INCREMENT_FILTERED_COUNT = Uri.withAppendedPath(
+                AUTHORITY_URI,
+                FILTERED_NUMBERS_INCREMENT_FILTERED_COUNT);
+
         /**
          * This utility class cannot be instantiated.
          */
diff --git a/src/com/android/dialer/database/FilteredNumberProvider.java b/src/com/android/dialer/database/FilteredNumberProvider.java
index 2bea7ca..acb5e7f 100644
--- a/src/com/android/dialer/database/FilteredNumberProvider.java
+++ b/src/com/android/dialer/database/FilteredNumberProvider.java
@@ -22,12 +22,12 @@
 import android.content.Context;
 import android.content.UriMatcher;
 import android.database.Cursor;
+import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.os.Binder;
 import android.text.TextUtils;
-import android.text.format.Time;
 import android.util.Log;
 
 import com.android.contacts.common.GeoUtil;
@@ -49,6 +49,7 @@
 
     private static final int FILTERED_NUMBERS_TABLE = 1;
     private static final int FILTERED_NUMBERS_TABLE_ID = 2;
+    private static final int FILTERED_NUMBERS_INCREMENT_FILTERED_COUNT = 3;
 
     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
 
@@ -64,6 +65,10 @@
         sUriMatcher.addURI(ObjectFactory.getFilteredNumberProviderAuthority(),
                 FilteredNumberContract.FilteredNumber.FILTERED_NUMBERS_TABLE + "/#",
                 FILTERED_NUMBERS_TABLE_ID);
+        sUriMatcher.addURI(ObjectFactory.getFilteredNumberProviderAuthority(),
+                FilteredNumberContract.FilteredNumber.FILTERED_NUMBERS_INCREMENT_FILTERED_COUNT
+                        + "/#",
+                FILTERED_NUMBERS_INCREMENT_FILTERED_COUNT);
         return true;
     }
 
@@ -121,9 +126,7 @@
 
     @VisibleForTesting
     protected long getCurrentTimeMs() {
-        Time timeNow = new Time();
-        timeNow.setToNow();
-        return timeNow.toMillis(false);
+        return System.currentTimeMillis();
     }
 
     private void setDefaultValues(ContentValues values) {
@@ -171,6 +174,20 @@
             case FILTERED_NUMBERS_TABLE_ID:
                 selection = getSelectionWithId(selection, ContentUris.parseId(uri));
                 break;
+            case FILTERED_NUMBERS_INCREMENT_FILTERED_COUNT:
+                final long id = ContentUris.parseId(uri);
+                try {
+                    db.execSQL(" UPDATE " + DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE
+                            + " SET" + FilteredNumberColumns.TIMES_FILTERED + "="
+                            + FilteredNumberColumns.TIMES_FILTERED + "+1,"
+                            + FilteredNumberColumns.LAST_TIME_FILTERED + "="
+                            + getCurrentTimeMs()
+                            + " WHERE " + FilteredNumberColumns._ID + "=" + id);
+                } catch (SQLException e) {
+                    Log.d(TAG, "Could not update blocked statistics for " + id);
+                    return 0;
+                }
+                return 1;
             default:
                 throw new IllegalArgumentException("Unknown uri: " + uri);
         }
diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java
index d2628da..54e4b89 100644
--- a/src/com/android/dialer/dialpad/DialpadFragment.java
+++ b/src/com/android/dialer/dialpad/DialpadFragment.java
@@ -81,6 +81,7 @@
 import com.android.dialer.SpecialCharSequenceMgr;
 import com.android.dialer.calllog.PhoneAccountUtils;
 import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.TelecomUtil;
 import com.android.dialer.util.IntentUtil.CallIntentBuilder;
 import com.android.incallui.Call.LogState;
 import com.android.phone.common.CallLogAsync;
@@ -1478,8 +1479,12 @@
      * @return true if the phone is "in use", meaning that at least one line
      *              is active (ie. off hook or ringing or dialing, or on hold).
      */
-    public boolean isPhoneInUse() {
-        return getTelecomManager().isInCall();
+    private boolean isPhoneInUse() {
+        final Context context = getActivity();
+        if (context != null) {
+            return TelecomUtil.isInCall(context);
+        }
+        return false;
     }
 
     /**
diff --git a/src/com/android/dialer/filterednumber/BlockedNumberAdapter.java b/src/com/android/dialer/filterednumber/BlockedNumberAdapter.java
new file mode 100644
index 0000000..504b520
--- /dev/null
+++ b/src/com/android/dialer/filterednumber/BlockedNumberAdapter.java
@@ -0,0 +1,148 @@
+/*
+ * 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.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.provider.ContactsContract;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.QuickContactBadge;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.R;
+import com.android.dialer.calllog.ContactInfo;
+import com.android.dialer.calllog.ContactInfoHelper;
+import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialer.util.PhoneNumberUtil;
+
+public class BlockedNumberAdapter extends SimpleCursorAdapter {
+
+    private Context mContext;
+    private ContactInfoHelper mContactInfoHelper;
+    private Resources mResources;
+    private BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
+    private ContactPhotoManager mContactPhotoManager;
+    private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+
+    public BlockedNumberAdapter(Context context,
+                                FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) {
+        super(context, R.layout.blocked_number_item, null, new String[]{}, new int[]{}, 0);
+        mContext = context;
+        mContactInfoHelper = new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context));
+        mContactPhotoManager = ContactPhotoManager.getInstance(context);
+        mResources = context.getResources();
+        mFilteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler;
+    }
+
+    @Override
+    public void bindView(View view, Context context, Cursor cursor) {
+        super.bindView(view, context, cursor);
+        final TextView callerName = (TextView) view.findViewById(R.id.caller_name);
+        final TextView callerNumber = (TextView) view.findViewById(R.id.caller_number);
+        final View deleteNumber = view.findViewById(R.id.delete_button);
+        final QuickContactBadge quickContactBadge =
+                (QuickContactBadge) view.findViewById(R.id.quick_contact_photo);
+        quickContactBadge.setOverlay(null);
+        quickContactBadge.setPrioritizedMimeType(
+                ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
+
+        final Integer id = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID));
+        final String countryIso = cursor.getString(cursor.getColumnIndex(
+                FilteredNumberColumns.COUNTRY_ISO));
+        final String number = cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.NUMBER));
+        final String normalizedNumber = cursor.getString(cursor.getColumnIndex(
+                FilteredNumberColumns.NORMALIZED_NUMBER));
+        final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
+        final CharSequence locationOrType = getNumberTypeOrLocation(info);
+        final String displayNumber = getDisplayNumber(info);
+        final String displayNumberStr = mBidiFormatter.unicodeWrap(
+                displayNumber.toString(), TextDirectionHeuristics.LTR);
+
+        deleteNumber.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                FilterNumberDialogFragment newFragment =
+                        FilterNumberDialogFragment.newInstance(id, normalizedNumber, number,
+                                countryIso, displayNumber);
+                newFragment.setQueryHandler(mFilteredNumberAsyncQueryHandler);
+                newFragment.setParentView(view);
+                newFragment.show(((Activity) mContext).getFragmentManager(),
+                        FilterNumberDialogFragment.BLOCK_DIALOG_FRAGMENT);
+            }
+        });
+
+        String nameForDefaultImage;
+        if (!TextUtils.isEmpty(info.name)) {
+            nameForDefaultImage = info.name;
+            callerName.setText(info.name);
+            callerNumber.setText(locationOrType + " " + displayNumberStr);
+        } else {
+            nameForDefaultImage = displayNumber;
+            callerName.setText(displayNumberStr);
+            if (!TextUtils.isEmpty(locationOrType)) {
+                callerNumber.setText(locationOrType);
+                callerNumber.setVisibility(View.VISIBLE);
+            } else {
+                callerNumber.setVisibility(View.GONE);
+            }
+        }
+        loadContactPhoto(info, nameForDefaultImage, quickContactBadge);
+    }
+
+    private void loadContactPhoto(ContactInfo info, String displayName, QuickContactBadge badge) {
+        final String lookupKey = info.lookupUri == null
+                ? null : UriUtils.getLookupKeyFromUri(info.lookupUri);
+        final int contactType = mContactInfoHelper.isBusiness(info.sourceType)
+                ? ContactPhotoManager.TYPE_BUSINESS : ContactPhotoManager.TYPE_DEFAULT;
+        final DefaultImageRequest request = new DefaultImageRequest(displayName, lookupKey,
+                contactType, true /* isCircular */);
+        badge.assignContactUri(info.lookupUri);
+        badge.setContentDescription(
+                mResources.getString(R.string.description_contact_details, displayName));
+        mContactPhotoManager.loadDirectoryPhoto(badge, info.photoUri,
+                false /* darkTheme */, true /* isCircular */, request);
+    }
+
+    private String getDisplayNumber(ContactInfo info) {
+        if (!TextUtils.isEmpty(info.formattedNumber)) {
+            return info.formattedNumber;
+        } else if (!TextUtils.isEmpty(info.number)) {
+            return info.number;
+        } else {
+            return "";
+        }
+    }
+
+    private CharSequence getNumberTypeOrLocation(ContactInfo info) {
+        if (!TextUtils.isEmpty(info.name)) {
+            return ContactsContract.CommonDataKinds.Phone.getTypeLabel(mResources, info.type,
+                    info.label);
+        } else {
+            return PhoneNumberUtil.getGeoDescription(mContext, info.number);
+        }
+    }
+}
diff --git a/src/com/android/dialer/filterednumber/BlockedNumberFragment.java b/src/com/android/dialer/filterednumber/BlockedNumberFragment.java
new file mode 100644
index 0000000..69fba34
--- /dev/null
+++ b/src/com/android/dialer/filterednumber/BlockedNumberFragment.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.ListFragment;
+import android.app.LoaderManager;
+import android.content.CursorLoader;
+import android.content.DialogInterface;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.dialog.IndeterminateProgressDialog;
+import com.android.dialer.R;
+import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.database.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener;
+import com.android.dialer.database.FilteredNumberContract;
+
+public class BlockedNumberFragment extends ListFragment implements
+        LoaderManager.LoaderCallbacks<Cursor>, View.OnClickListener {
+
+    private BlockedNumberAdapter mAdapter;
+    private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        LayoutInflater inflater = LayoutInflater.from(getContext());
+        getListView().addHeaderView(inflater.inflate(R.layout.blocked_number_header, null));
+        mFilteredNumberAsyncQueryHandler =
+                new FilteredNumberAsyncQueryHandler(getActivity().getContentResolver());
+        if (mAdapter == null) {
+            mAdapter = new BlockedNumberAdapter(getContext(), mFilteredNumberAsyncQueryHandler);
+        }
+        setListAdapter(mAdapter);
+        final Button addNumberBtn = (Button) getActivity().findViewById(R.id.add_number_button);
+        addNumberBtn.setOnClickListener(this);
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        setListAdapter(null);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        getLoaderManager().initLoader(0, null, this);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.blocked_number_fragment, container, false);
+        return view;
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        final String[] projection = {
+            FilteredNumberContract.FilteredNumberColumns._ID,
+            FilteredNumberContract.FilteredNumberColumns.COUNTRY_ISO,
+            FilteredNumberContract.FilteredNumberColumns.NUMBER,
+            FilteredNumberContract.FilteredNumberColumns.NORMALIZED_NUMBER
+        };
+        final String selection = FilteredNumberContract.FilteredNumberColumns.TYPE
+                + "=" + FilteredNumberContract.FilteredNumberTypes.BLOCKED_NUMBER;
+        final CursorLoader cursorLoader = new CursorLoader(
+                getContext(), FilteredNumberContract.FilteredNumber.CONTENT_URI, projection,
+                selection, null, null);
+        return cursorLoader;
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+        mAdapter.swapCursor(data);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+        mAdapter.swapCursor(null);
+    }
+
+    @Override
+    public void onClick(final View v) {
+        final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
+        final EditText numberField = new EditText(getContext());
+        final DialogInterface.OnClickListener okListener = new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int whichButton) {
+                final String number = numberField.getText().toString();
+                final IndeterminateProgressDialog progressDialog =
+                        IndeterminateProgressDialog.show(getFragmentManager(),
+                                getString(R.string.checkingNumber, number), null, 1000);
+                final String normalizedNumber =
+                        FilteredNumberAsyncQueryHandler.getNormalizedNumber(number, countryIso);
+                if (normalizedNumber == null) {
+                    progressDialog.dismiss();
+                    Toast.makeText(getContext(), getString(R.string.invalidNumber, number),
+                            Toast.LENGTH_LONG).show();
+                } else {
+                    final OnCheckBlockedListener onCheckListener = new OnCheckBlockedListener() {
+                        @Override
+                        public void onCheckComplete(Integer id) {
+                            progressDialog.dismiss();
+                            if (id == null) {
+                                FilterNumberDialogFragment newFragment =
+                                        FilterNumberDialogFragment.newInstance(id, normalizedNumber,
+                                                number, countryIso, number);
+                                newFragment.setQueryHandler(mFilteredNumberAsyncQueryHandler);
+                                newFragment.setParentView(v);
+                                newFragment.show(getActivity().getFragmentManager(),
+                                        FilterNumberDialogFragment.BLOCK_DIALOG_FRAGMENT);
+                            } else {
+                                Toast.makeText(getContext(),
+                                        getString(R.string.alreadyBlocked, number),
+                                        Toast.LENGTH_LONG).show();
+                            }
+                        }
+                    };
+                    mFilteredNumberAsyncQueryHandler.startBlockedQuery(
+                            onCheckListener, normalizedNumber, number, countryIso);
+                }
+            }
+        };
+        new AlertDialog.Builder(getContext())
+                .setTitle(getString(R.string.blockNumber))
+                .setView(numberField)
+                .setPositiveButton(getString(R.string.blockNumberOk), okListener)
+                .setNegativeButton(android.R.string.cancel, null)
+                .show();
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java b/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java
index f94d0f8..e9a88c8 100644
--- a/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java
+++ b/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java
@@ -39,11 +39,16 @@
     private static final String ARG_DISPLAY_NUMBER = "argDisplayNumber";
 
     private FilteredNumberAsyncQueryHandler mHandler;
+    private View mParentView;
 
     public void setQueryHandler (FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) {
         mHandler = filteredNumberAsyncQueryHandler;
     }
 
+    public void setParentView(View view) {
+        mParentView = view;
+    }
+
     public static FilterNumberDialogFragment newInstance(Integer blockId, String normalizedNumber,
         String number, String countryIso, String displayNumber) {
         final FilterNumberDialogFragment fragment = new FilterNumberDialogFragment();
@@ -91,7 +96,6 @@
     }
 
     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);
@@ -99,7 +103,7 @@
                 new FilteredNumberAsyncQueryHandler.OnUnblockNumberListener() {
                     @Override
                     public void onUnblockComplete(int rows, ContentValues values) {
-                        Snackbar.make(view, undoMessage, Snackbar.LENGTH_LONG).show();
+                        Snackbar.make(mParentView, undoMessage, Snackbar.LENGTH_LONG).show();
                     }
                 };
 
@@ -107,7 +111,7 @@
                 new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() {
                     @Override
                     public void onBlockComplete(final Uri uri) {
-                        Snackbar.make(view, message, Snackbar.LENGTH_LONG)
+                        Snackbar.make(mParentView, message, Snackbar.LENGTH_LONG)
                                 .setAction(R.string.block_number_undo,
                                         // Delete the newly created row on 'undo'.
                                         new View.OnClickListener() {
@@ -123,7 +127,6 @@
     }
 
     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);
@@ -131,14 +134,14 @@
                 new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() {
                     @Override
                     public void onBlockComplete(final Uri uri) {
-                        Snackbar.make(view, undoMessage, Snackbar.LENGTH_LONG).show();
+                        Snackbar.make(mParentView, 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)
+                        Snackbar.make(mParentView, message, Snackbar.LENGTH_LONG)
                                 .setAction(R.string.block_number_undo,
                                         new View.OnClickListener() {
                                             // Re-insert the row on 'undo', with a new ID.
diff --git a/src/com/android/dialer/list/RegularSearchFragment.java b/src/com/android/dialer/list/RegularSearchFragment.java
index 5c3117d..ec771e8 100644
--- a/src/com/android/dialer/list/RegularSearchFragment.java
+++ b/src/com/android/dialer/list/RegularSearchFragment.java
@@ -15,12 +15,10 @@
  */
 package com.android.dialer.list;
 
-import static android.Manifest.permission.ACCESS_FINE_LOCATION;
 import static android.Manifest.permission.READ_CONTACTS;
 
 import android.app.Activity;
 import android.content.pm.PackageManager;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 
diff --git a/src/com/android/dialer/settings/DialerSettingsActivity.java b/src/com/android/dialer/settings/DialerSettingsActivity.java
index 01a9fcf..2b72772 100644
--- a/src/com/android/dialer/settings/DialerSettingsActivity.java
+++ b/src/com/android/dialer/settings/DialerSettingsActivity.java
@@ -15,24 +15,20 @@
  */
 package com.android.dialer.settings;
 
-import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.os.Bundle;
-import android.os.Process;
 import android.os.UserManager;
-import android.preference.PreferenceActivity;
 import android.preference.PreferenceManager;
 import android.provider.Settings;
 import android.telecom.TelecomManager;
 import android.telephony.TelephonyManager;
-import android.util.Log;
 import android.view.MenuItem;
 import android.widget.Toast;
 
-import com.android.contacts.common.util.PermissionsUtil;
 import com.android.dialer.R;
+import com.android.dialer.filterednumber.BlockedNumberFragment;
 
 import java.util.List;
 
@@ -90,6 +86,11 @@
                 target.add(phoneAccountSettingsHeader);
             }
 
+            Header blockedCallsHeader = new Header();
+            blockedCallsHeader.titleRes = R.string.blocked_calls_settings_label;
+            blockedCallsHeader.fragment = BlockedNumberFragment.class.getName();
+            target.add(blockedCallsHeader);
+
             if (telephonyManager.isTtyModeSupported()
                     || telephonyManager.isHearingAidCompatibilitySupported()) {
                 Header accessibilitySettingsHeader = new Header();
diff --git a/src/com/android/dialer/util/DialerUtils.java b/src/com/android/dialer/util/DialerUtils.java
index fbe14ba..8870f76 100644
--- a/src/com/android/dialer/util/DialerUtils.java
+++ b/src/com/android/dialer/util/DialerUtils.java
@@ -33,18 +33,12 @@
 import android.text.TextUtils;
 import android.view.View;
 import android.view.inputmethod.InputMethodManager;
-import android.widget.ImageView;
-import android.widget.TextView;
 import android.widget.Toast;
 
 import com.android.contacts.common.ContactsUtils;
 import com.android.contacts.common.interactions.TouchPointManager;
 import com.android.dialer.R;
-import com.android.dialer.widget.EmptyContentView;
-import com.android.incallui.CallCardFragment;
-import com.android.incallui.Log;
 
-import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
@@ -94,7 +88,14 @@
                 }
                 final TelecomManager tm =
                         (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
-                tm.placeCall(intent.getData(), intent.getExtras());
+                if (TelecomUtil.hasCallPhonePermission(context)) {
+                    tm.placeCall(intent.getData(), intent.getExtras());
+                } else {
+                    // TODO: Make calling activity show request permission dialog and handle
+                    // callback results appropriately.
+                    Toast.makeText(context, "Cannot place call without Phone permission",
+                            Toast.LENGTH_SHORT);
+                }
             } else {
                 context.startActivity(intent);
             }
diff --git a/src/com/android/dialer/util/PhoneNumberUtil.java b/src/com/android/dialer/util/PhoneNumberUtil.java
index 84f58aa..539d8b9 100644
--- a/src/com/android/dialer/util/PhoneNumberUtil.java
+++ b/src/com/android/dialer/util/PhoneNumberUtil.java
@@ -25,13 +25,19 @@
 import android.util.Pair;
 
 import com.android.contacts.common.util.PhoneNumberHelper;
+import com.android.contacts.common.util.TelephonyManagerUtils;
 import com.google.common.collect.Sets;
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.Phonenumber;
+import com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder;
 
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
 public class PhoneNumberUtil {
+    private static final String TAG = "PhoneNumberUtil";
     private static final Set<String> LEGACY_UNKNOWN_NUMBERS = Sets.newHashSet("-1", "-2", "-3");
 
     /** Returns true if it is possible to place a call to the given number. */
@@ -92,4 +98,41 @@
     public static boolean isLegacyUnknownNumbers(CharSequence number) {
         return number != null && LEGACY_UNKNOWN_NUMBERS.contains(number.toString());
     }
+
+    /**
+     * @return a geographical description string for the specified number.
+     * @see com.android.i18n.phonenumbers.PhoneNumberOfflineGeocoder
+     */
+    public static String getGeoDescription(Context context, String number) {
+        Log.v(TAG, "getGeoDescription('" + number + "')...");
+
+        if (TextUtils.isEmpty(number)) {
+            return null;
+        }
+
+        com.google.i18n.phonenumbers.PhoneNumberUtil util =
+                com.google.i18n.phonenumbers.PhoneNumberUtil.getInstance();
+        PhoneNumberOfflineGeocoder geocoder = PhoneNumberOfflineGeocoder.getInstance();
+
+        Locale locale = context.getResources().getConfiguration().locale;
+        String countryIso = TelephonyManagerUtils.getCurrentCountryIso(context, locale);
+        Phonenumber.PhoneNumber pn = null;
+        try {
+            Log.v(TAG, "parsing '" + number
+                    + "' for countryIso '" + countryIso + "'...");
+            pn = util.parse(number, countryIso);
+            Log.v(TAG, "- parsed number: " + pn);
+        } catch (NumberParseException e) {
+            Log.v(TAG, "getGeoDescription: NumberParseException for incoming number '" +
+                    number + "'");
+        }
+
+        if (pn != null) {
+            String description = geocoder.getDescriptionForNumber(pn, locale);
+            Log.v(TAG, "- got description: '" + description + "'");
+            return description;
+        }
+
+        return null;
+    }
 }
diff --git a/src/com/android/dialer/util/TelecomUtil.java b/src/com/android/dialer/util/TelecomUtil.java
index 1cd270c..43b9a72 100644
--- a/src/com/android/dialer/util/TelecomUtil.java
+++ b/src/com/android/dialer/util/TelecomUtil.java
@@ -26,6 +26,9 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public class TelecomUtil {
     private static final String TAG = "TelecomUtil";
     private static boolean sWarningLogged = false;
@@ -78,6 +81,20 @@
         return false;
     }
 
+    public static List<PhoneAccountHandle> getCallCapablePhoneAccounts(Context context) {
+        if (hasReadPhoneStatePermission(context)) {
+            return getTelecomManager(context).getCallCapablePhoneAccounts();
+        }
+        return new ArrayList<>();
+    }
+
+    public static boolean isInCall(Context context) {
+        if (hasReadPhoneStatePermission(context)) {
+            return getTelecomManager(context).isInCall();
+        }
+        return false;
+    }
+
     public static Uri getCallLogUri(Context context) {
         return hasReadWriteVoicemailPermissions(context) ? Calls.CONTENT_URI_WITH_VOICEMAIL
                 : Calls.CONTENT_URI;
@@ -94,6 +111,16 @@
                 || hasPermission(context, Manifest.permission.MODIFY_PHONE_STATE);
     }
 
+    public static boolean hasReadPhoneStatePermission(Context context) {
+        return isDefaultDialer(context)
+                || hasPermission(context, Manifest.permission.READ_PHONE_STATE);
+    }
+
+    public static boolean hasCallPhonePermission(Context context) {
+        return isDefaultDialer(context)
+                || hasPermission(context, Manifest.permission.CALL_PHONE);
+    }
+
     private static boolean hasPermission(Context context, String permission) {
         return context.checkSelfPermission(permission)
                 == PackageManager.PERMISSION_GRANTED;
diff --git a/src/com/android/dialer/voicemail/VoicemailAudioManager.java b/src/com/android/dialer/voicemail/VoicemailAudioManager.java
index e64e180..267eeca 100644
--- a/src/com/android/dialer/voicemail/VoicemailAudioManager.java
+++ b/src/com/android/dialer/voicemail/VoicemailAudioManager.java
@@ -19,25 +19,36 @@
 import android.content.Context;
 import android.media.AudioManager;
 import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.telecom.CallAudioState;
 import android.util.Log;
 
+import java.util.Objects;
 import java.util.concurrent.RejectedExecutionException;
 
 /**
  * This class manages all audio changes for voicemail playback.
  */
-final class VoicemailAudioManager implements OnAudioFocusChangeListener {
+final class VoicemailAudioManager implements OnAudioFocusChangeListener,
+        WiredHeadsetManager.Listener {
     private static final String TAG = VoicemailAudioManager.class.getSimpleName();
 
     public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL;
 
     private AudioManager mAudioManager;
     private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+    private WiredHeadsetManager mWiredHeadsetManager;
+    private boolean mWasSpeakerOn;
+    private CallAudioState mCallAudioState;
 
     public VoicemailAudioManager(Context context,
             VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
         mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
+        mWiredHeadsetManager = new WiredHeadsetManager(context);
+        mWiredHeadsetManager.setListener(this);
+
+        mCallAudioState = getInitialAudioState();
+        Log.i(TAG, "Initial audioState = " + mCallAudioState);
     }
 
     public void requestAudioFocus() {
@@ -60,14 +71,131 @@
         mVoicemailPlaybackPresenter.onAudioFocusChange(focusChange == AudioManager.AUDIOFOCUS_GAIN);
     }
 
-    public void turnOnSpeaker(boolean on) {
+    @Override
+    public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+        Log.i(TAG, "wired headset was plugged in changed: " + oldIsPluggedIn
+                + " -> "+ newIsPluggedIn);
+
+        if (oldIsPluggedIn == newIsPluggedIn) {
+            return;
+        }
+
+        int newRoute = mCallAudioState.getRoute();  // start out with existing route
+        if (newIsPluggedIn) {
+            newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
+        } else {
+            if (mWasSpeakerOn) {
+                newRoute = CallAudioState.ROUTE_SPEAKER;
+            } else {
+                newRoute = CallAudioState.ROUTE_EARPIECE;
+            }
+        }
+
+        mVoicemailPlaybackPresenter.setSpeakerphoneOn(newRoute == CallAudioState.ROUTE_SPEAKER);
+
+        // We need to call this every time even if we do not change the route because the supported
+        // routes changed either to include or not include WIRED_HEADSET.
+        setSystemAudioState(
+                new CallAudioState(false /* muted */, newRoute, calculateSupportedRoutes()));
+    }
+
+    public void setSpeakerphoneOn(boolean on) {
+        setAudioRoute(on ? CallAudioState.ROUTE_SPEAKER : CallAudioState.ROUTE_WIRED_OR_EARPIECE);
+    }
+
+    public boolean isWiredHeadsetPluggedIn() {
+        return mWiredHeadsetManager.isPluggedIn();
+    }
+
+    public void registerReceivers() {
+        // Receivers is plural because we expect to add bluetooth support.
+        mWiredHeadsetManager.registerReceiver();
+    }
+
+    public void unregisterReceivers() {
+        mWiredHeadsetManager.unregisterReceiver();
+    }
+
+    /**
+     * Change the audio route, for example from earpiece to speakerphone.
+     *
+     * @param route The new audio route to use. See {@link CallAudioState}.
+     */
+    void setAudioRoute(int route) {
+        Log.v(TAG, "setAudioRoute, route: " + CallAudioState.audioRouteToString(route));
+
+        // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
+        int newRoute = selectWiredOrEarpiece(route, mCallAudioState.getSupportedRouteMask());
+
+        // If route is unsupported, do nothing.
+        if ((mCallAudioState.getSupportedRouteMask() | newRoute) == 0) {
+            Log.w(TAG, "Asking to set to a route that is unsupported: " + newRoute);
+            return;
+        }
+
+        if (mCallAudioState.getRoute() != newRoute) {
+            // Remember the new speaker state so it can be restored when the user plugs and unplugs
+            // a headset.
+            mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;
+            setSystemAudioState(new CallAudioState(false /* muted */, newRoute,
+                    mCallAudioState.getSupportedRouteMask()));
+        }
+    }
+
+    private CallAudioState getInitialAudioState() {
+        int supportedRouteMask = calculateSupportedRoutes();
+        int route = selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE,
+                supportedRouteMask);
+        return new CallAudioState(false /* muted */, route, supportedRouteMask);
+    }
+
+    private int calculateSupportedRoutes() {
+        int routeMask = CallAudioState.ROUTE_SPEAKER;
+        if (mWiredHeadsetManager.isPluggedIn()) {
+            routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
+        } else {
+            routeMask |= CallAudioState.ROUTE_EARPIECE;
+        }
+        return routeMask;
+    }
+
+    private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
+        // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
+        // ROUTE_WIRED_OR_EARPIECE so that callers don't have to make a call to check which is
+        // supported before calling setAudioRoute.
+        if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) {
+            route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
+            if (route == 0) {
+                Log.wtf(TAG, "One of wired headset or earpiece should always be valid.");
+                // assume earpiece in this case.
+                route = CallAudioState.ROUTE_EARPIECE;
+            }
+        }
+        return route;
+    }
+
+    private void setSystemAudioState(CallAudioState callAudioState) {
+        CallAudioState oldAudioState = mCallAudioState;
+        mCallAudioState = callAudioState;
+
+        Log.i(TAG, "setSystemAudioState: changing from " + oldAudioState + " to "
+                + mCallAudioState);
+
+        // Audio route.
+        if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
+            turnOnSpeaker(true);
+        } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE ||
+                mCallAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) {
+            // Just handle turning off the speaker, the system will handle switching between wired
+            // headset and earpiece.
+            turnOnSpeaker(false);
+        }
+    }
+
+    private void turnOnSpeaker(boolean on) {
         if (mAudioManager.isSpeakerphoneOn() != on) {
             Log.i(TAG, "turning speaker phone on: " + on);
             mAudioManager.setSpeakerphoneOn(on);
         }
     }
-
-    public boolean isSpeakerphoneOn() {
-        return mAudioManager.isSpeakerphoneOn();
-    }
 }
\ No newline at end of file
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
index 14c5473..f86fc55 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
@@ -19,6 +19,7 @@
 import android.app.Activity;
 import android.app.Fragment;
 import android.content.Context;
+import android.graphics.drawable.Drawable;
 import android.media.MediaPlayer;
 import android.net.Uri;
 import android.os.Bundle;
@@ -238,6 +239,8 @@
     private TextView mTotalDurationText;
 
     private PositionUpdater mPositionUpdater;
+    private Drawable mVoicemailSeekHandleEnabled;
+    private Drawable mVoicemailSeekHandleDisabled;
 
     public VoicemailPlaybackLayout(Context context) {
         this(context, null);
@@ -276,9 +279,12 @@
         mDeleteButton.setOnClickListener(mDeleteButtonListener);
 
         mPositionText.setText(formatAsMinutesAndSeconds(0));
-        mPositionText.setVisibility(View.INVISIBLE);
         mTotalDurationText.setText(formatAsMinutesAndSeconds(0));
-        mTotalDurationText.setVisibility(View.INVISIBLE);
+
+        mVoicemailSeekHandleEnabled = getResources().getDrawable(
+                R.drawable.ic_voicemail_seek_handle, mContext.getTheme());
+        mVoicemailSeekHandleDisabled = getResources().getDrawable(
+                R.drawable.ic_voicemail_seek_handle_disabled, mContext.getTheme());
     }
 
     @Override
@@ -317,6 +323,7 @@
         mStateText.setText(getString(R.string.voicemail_playback_error));
     }
 
+    @Override
     public void onSpeakerphoneOn(boolean on) {
         if (on) {
             mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_up_24dp);
@@ -354,7 +361,7 @@
 
     @Override
     public void setFetchContentTimeout() {
-        disableUiElements();
+        mStartStopButton.setEnabled(true);
         mStateText.setText(getString(R.string.voicemail_fetching_timout));
     }
 
@@ -366,20 +373,22 @@
     @Override
     public void disableUiElements() {
         mStartStopButton.setEnabled(false);
-        mPlaybackSeek.setProgress(0);
         mPlaybackSeek.setEnabled(false);
-
-        mPositionText.setText(formatAsMinutesAndSeconds(0));
-        mTotalDurationText.setText(formatAsMinutesAndSeconds(0));
+        mPlaybackSeek.setThumb(mVoicemailSeekHandleDisabled);
     }
 
     @Override
     public void enableUiElements() {
         mStartStopButton.setEnabled(true);
         mPlaybackSeek.setEnabled(true);
+        mPlaybackSeek.setThumb(mVoicemailSeekHandleEnabled);
+    }
 
-        mPositionText.setVisibility(View.VISIBLE);
-        mTotalDurationText.setVisibility(View.VISIBLE);
+    @Override
+    public void resetSeekBar() {
+        mPlaybackSeek.setProgress(0);
+        mPlaybackSeek.setEnabled(false);
+        mPlaybackSeek.setThumb(mVoicemailSeekHandleDisabled);
     }
 
     @Override
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
index 95622bf..8b8b7c5 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -85,6 +85,7 @@
         void setFetchContentTimeout();
         void setIsFetchingContent();
         void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
+        void resetSeekBar();
     }
 
     public interface OnVoicemailDeletedListener {
@@ -101,6 +102,7 @@
 
     private static final String[] HAS_CONTENT_PROJECTION = new String[] {
         VoicemailContract.Voicemails.HAS_CONTENT,
+        VoicemailContract.Voicemails.DURATION
     };
 
     private static final int NUMBER_OF_THREADS_IN_POOL = 2;
@@ -251,19 +253,19 @@
                 mPosition = 0;
                 // Default to earpiece.
                 setSpeakerphoneOn(false);
+                mVoicemailAudioManager.setSpeakerphoneOn(false);
             } else {
                 // Update the view to the current speakerphone state.
                 mView.onSpeakerphoneOn(mIsSpeakerphoneOn);
             }
 
-            mDuration.set(0);
+            checkForContent();
 
             if (startPlayingImmediately) {
                 // Since setPlaybackView can get called during the view binding process, we don't
                 // want to reset mIsPlaying to false if the user is currently playing the
                 // voicemail and the view is rebound.
                 mIsPlaying = startPlayingImmediately;
-                checkForContent();
             }
         }
     }
@@ -272,16 +274,20 @@
      * Reset the presenter for playback back to its original state.
      */
     public void resetAll() {
-        reset();
+        pausePresenter(true);
 
         mView = null;
         mVoicemailUri = null;
     }
 
     /**
-     * Reset the presenter such that it is as if the voicemail has not been played.
+     * When navigating away from voicemail playback, we need to release the media player,
+     * pause the UI and save the position.
+     *
+     * @param reset {@code true} if we want to reset the position of the playback, {@code false} if
+     * we want to retain the current position (in case we return to the voicemail).
      */
-    public void reset() {
+    public void pausePresenter(boolean reset) {
         if (mMediaPlayer != null) {
             mMediaPlayer.release();
             mMediaPlayer = null;
@@ -291,19 +297,35 @@
 
         mIsPrepared = false;
         mIsPlaying = false;
-        mPosition = 0;
-        mDuration.set(0);
+
+        if (reset) {
+            // We want to reset the position whether or not the view is valid.
+            mPosition = 0;
+        }
 
         if (mView != null) {
             mView.onPlaybackStopped();
-            mView.setClipPosition(0, mDuration.get());
+            if (reset) {
+                mView.setClipPosition(0, mDuration.get());
+            } else {
+                mPosition = mView.getDesiredClipPosition();
+            }
         }
     }
 
     /**
+     * Must be invoked when the parent activity is resumed.
+     */
+    public void onResume() {
+        mVoicemailAudioManager.registerReceivers();
+    }
+
+    /**
      * Must be invoked when the parent activity is paused.
      */
     public void onPause() {
+        mVoicemailAudioManager.unregisterReceivers();
+
         if (mContext != null && mIsPrepared
                 && mInitialOrientation != mContext.getResources().getConfiguration().orientation) {
             // If an orientation change triggers the pause, retain the MediaPlayer.
@@ -312,11 +334,12 @@
         }
 
         // Release the media player, otherwise there may be failures.
-        reset();
+        pausePresenter(false);
 
         if (mActivity != null) {
             mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
         }
+
     }
 
     /**
@@ -345,8 +368,8 @@
      * voicemail we've been asked to play has any content available.
      * <p>
      * Notify the user that we are fetching the content, then check to see if the content field in
-     * the DB is set. If set, we proceed to {@link #prepareContent()} method. If not set, make
-     * a request to fetch the content asynchronously via {@link #requestContent()}.
+     * the DB is set. If set, we proceed to {@link #prepareContent()} method. We get the duration of
+     * the voicemail from the query and set it if the content is not available.
      */
     private void checkForContent() {
         mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() {
@@ -360,7 +383,8 @@
                 if (hasContent) {
                     prepareContent();
                 } else {
-                    requestContent();
+                    mView.resetSeekBar();
+                    mView.setClipPosition(0, mDuration.get());
                 }
             }
         });
@@ -373,10 +397,14 @@
 
         ContentResolver contentResolver = mContext.getContentResolver();
         Cursor cursor = contentResolver.query(
-                voicemailUri, HAS_CONTENT_PROJECTION, null, null, null);
+                voicemailUri, null, null, null, null);
         try {
             if (cursor != null && cursor.moveToNext()) {
-                return cursor.getInt(cursor.getColumnIndexOrThrow(
+                int duration = cursor.getInt(cursor.getColumnIndex(
+                        VoicemailContract.Voicemails.DURATION));
+                // Convert database duration (seconds) into mDuration (milliseconds)
+                mDuration.set(duration > 0 ? duration * 1000 : 0);
+                return cursor.getInt(cursor.getColumnIndex(
                         VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
             }
         } finally {
@@ -519,7 +547,6 @@
         mIsPrepared = true;
 
         mDuration.set(mMediaPlayer.getDuration());
-        mPosition = mMediaPlayer.getCurrentPosition();
 
         Log.d(TAG, "onPrepared: mPosition=" + mPosition);
         mView.setClipPosition(mPosition, mDuration.get());
@@ -604,7 +631,7 @@
 
         if (!mIsPrepared) {
             // If we haven't downloaded the voicemail yet, attempt to download it.
-            checkForContent();
+            requestContent();
             mIsPlaying = true;
             return;
         }
@@ -614,15 +641,15 @@
         if (!mMediaPlayer.isPlaying()) {
             // Clamp the start position between 0 and the duration.
             mPosition = Math.max(0, Math.min(mPosition, mDuration.get()));
+
             mMediaPlayer.seekTo(mPosition);
 
             try {
                 // Grab audio focus.
                 // Can throw RejectedExecutionException.
                 mVoicemailAudioManager.requestAudioFocus();
-
-                setSpeakerphoneOn(mIsSpeakerphoneOn);
                 mMediaPlayer.start();
+                setSpeakerphoneOn(mIsSpeakerphoneOn);
             } catch (RejectedExecutionException e) {
                 handleError(e);
             }
@@ -708,21 +735,29 @@
         }
     }
 
+    /**
+     * This is for use by UI interactions only. It simplifies UI logic.
+     */
     public void toggleSpeakerphone() {
+        mVoicemailAudioManager.setSpeakerphoneOn(!mIsSpeakerphoneOn);
         setSpeakerphoneOn(!mIsSpeakerphoneOn);
     }
 
-    private void setSpeakerphoneOn(boolean on) {
+    /**
+     * This method only handles app-level changes to the speakerphone. Audio layer changes should
+     * be handled separately. This is so that the VoicemailAudioManager can trigger changes to
+     * the presenter without the presenter triggering the audio manager and duplicating actions.
+     */
+    public void setSpeakerphoneOn(boolean on) {
         mView.onSpeakerphoneOn(on);
-        if (mIsSpeakerphoneOn == on) {
-            return;
-        }
 
         mIsSpeakerphoneOn = on;
-        mVoicemailAudioManager.turnOnSpeaker(on);
 
+        // This should run even if speakerphone is not being toggled because we may be switching
+        // from earpiece to headphone and vise versa. Also upon initial setup the default audio
+        // source is the earpiece, so we want to trigger the proximity sensor.
         if (mIsPlaying) {
-            if (on) {
+            if (on || mVoicemailAudioManager.isWiredHeadsetPluggedIn()) {
                 disableProximitySensor(false /* waitForFarState */);
                 if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
                     mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
diff --git a/src/com/android/dialer/voicemail/WiredHeadsetManager.java b/src/com/android/dialer/voicemail/WiredHeadsetManager.java
new file mode 100644
index 0000000..7351f4f
--- /dev/null
+++ b/src/com/android/dialer/voicemail/WiredHeadsetManager.java
@@ -0,0 +1,88 @@
+/*
+ * 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.voicemail;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.util.Log;
+
+/** Listens for and caches headset state. */
+class WiredHeadsetManager {
+    private static final String TAG = WiredHeadsetManager.class.getSimpleName();
+
+    interface Listener {
+        void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn);
+    }
+
+    /** Receiver for wired headset plugged and unplugged events. */
+    private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (AudioManager.ACTION_HEADSET_PLUG.equals(intent.getAction())) {
+                boolean isPluggedIn = intent.getIntExtra("state", 0) == 1;
+                Log.v(TAG, "ACTION_HEADSET_PLUG event, plugged in: " + isPluggedIn);
+                onHeadsetPluggedInChanged(isPluggedIn);
+            }
+        }
+    }
+
+    private final WiredHeadsetBroadcastReceiver mReceiver;
+    private boolean mIsPluggedIn;
+    private Listener mListener;
+    private Context mContext;
+
+    WiredHeadsetManager(Context context) {
+        mContext = context;
+        mReceiver = new WiredHeadsetBroadcastReceiver();
+
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mIsPluggedIn = audioManager.isWiredHeadsetOn();
+
+    }
+
+    void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    boolean isPluggedIn() {
+        return mIsPluggedIn;
+    }
+
+    void registerReceiver() {
+        // Register for misc other intent broadcasts.
+        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+        mContext.registerReceiver(mReceiver, intentFilter);
+    }
+
+    void unregisterReceiver() {
+        mContext.unregisterReceiver(mReceiver);
+    }
+
+    private void onHeadsetPluggedInChanged(boolean isPluggedIn) {
+        if (mIsPluggedIn != isPluggedIn) {
+            Log.v(TAG, "onHeadsetPluggedInChanged, mIsPluggedIn: " + mIsPluggedIn + " -> "
+                    + isPluggedIn);
+            boolean oldIsPluggedIn = mIsPluggedIn;
+            mIsPluggedIn = isPluggedIn;
+            if (mListener != null) {
+                mListener.onWiredHeadsetPluggedInChanged(oldIsPluggedIn, mIsPluggedIn);
+            }
+        }
+    }
+}
\ No newline at end of file