Merge "Copy "Blocked Numbers Activity" from Dialer to Contacts" into ub-contactsdialer-b-dev
diff --git a/Android.mk b/Android.mk
index ad13292..e4d7382 100644
--- a/Android.mk
+++ b/Android.mk
@@ -21,7 +21,8 @@
 LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
 LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) \
     $(support_library_root_dir)/v7/appcompat/res \
-    $(support_library_root_dir)/v7/cardview/res
+    $(support_library_root_dir)/v7/cardview/res \
+    $(support_library_root_dir)/design/res
 LOCAL_ASSET_DIR := $(addprefix $(LOCAL_PATH)/, $(asset_dirs))
 
 LOCAL_AAPT_FLAGS := \
@@ -29,6 +30,7 @@
     --extra-packages com.android.contacts.common \
     --extra-packages com.android.phone.common \
     --extra-packages android.support.v7.appcompat \
+    --extra-packages android.support.design \
     --extra-packages android.support.v7.cardview
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
@@ -40,6 +42,7 @@
     android-support-v7-cardview \
     android-support-v7-palette \
     android-support-v4 \
+    android-support-design \
     libphonenumber
 
 LOCAL_PACKAGE_NAME := Contacts
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b3d47db..32016b2 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -418,6 +418,17 @@
             </intent-filter>
         </activity>
 
+        <!-- Blocked numbers activity -->
+        <activity android:name=".activities.BlockedNumbersActivity"
+                  android:label="@string/blocked_numbers_title"
+                  android:theme="@style/BlockedNumbersStyle">
+            <intent-filter>
+                <action android:name="android.intent.action.EDIT" />
+                <data android:mimeType="blocked_numbers/*" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
         <!-- vCard related -->
         <activity android:name=".common.vcard.ImportVCardActivity"
             android:label="@string/launcherActivityLabel"
diff --git a/res/drawable-hdpi/empty_contacts.png b/res/drawable-hdpi/empty_contacts.png
new file mode 100644
index 0000000..d3c0378
--- /dev/null
+++ b/res/drawable-hdpi/empty_contacts.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_not_interested_googblue_24dp.png b/res/drawable-hdpi/ic_not_interested_googblue_24dp.png
new file mode 100644
index 0000000..26a26f9
--- /dev/null
+++ b/res/drawable-hdpi/ic_not_interested_googblue_24dp.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_remove.png b/res/drawable-hdpi/ic_remove.png
new file mode 100644
index 0000000..1ee6adf
--- /dev/null
+++ b/res/drawable-hdpi/ic_remove.png
Binary files differ
diff --git a/res/drawable-hdpi/search_shadow.9.png b/res/drawable-hdpi/search_shadow.9.png
new file mode 100644
index 0000000..92b4f5b
--- /dev/null
+++ b/res/drawable-hdpi/search_shadow.9.png
Binary files differ
diff --git a/res/drawable-mdpi/empty_contacts.png b/res/drawable-mdpi/empty_contacts.png
new file mode 100644
index 0000000..2ce7eae
--- /dev/null
+++ b/res/drawable-mdpi/empty_contacts.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_not_interested_googblue_24dp.png b/res/drawable-mdpi/ic_not_interested_googblue_24dp.png
new file mode 100644
index 0000000..d7d5c58
--- /dev/null
+++ b/res/drawable-mdpi/ic_not_interested_googblue_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_remove.png b/res/drawable-mdpi/ic_remove.png
new file mode 100644
index 0000000..2c134ea
--- /dev/null
+++ b/res/drawable-mdpi/ic_remove.png
Binary files differ
diff --git a/res/drawable-mdpi/search_shadow.9.png b/res/drawable-mdpi/search_shadow.9.png
new file mode 100644
index 0000000..0c33905
--- /dev/null
+++ b/res/drawable-mdpi/search_shadow.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/empty_contacts.png b/res/drawable-xhdpi/empty_contacts.png
new file mode 100644
index 0000000..65b1de3
--- /dev/null
+++ b/res/drawable-xhdpi/empty_contacts.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png b/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png
new file mode 100644
index 0000000..3e6ec07
--- /dev/null
+++ b/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_remove.png b/res/drawable-xhdpi/ic_remove.png
new file mode 100644
index 0000000..be81592
--- /dev/null
+++ b/res/drawable-xhdpi/ic_remove.png
Binary files differ
diff --git a/res/drawable-xhdpi/search_shadow.9.png b/res/drawable-xhdpi/search_shadow.9.png
new file mode 100644
index 0000000..5667ab3
--- /dev/null
+++ b/res/drawable-xhdpi/search_shadow.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/empty_contacts.png b/res/drawable-xxhdpi/empty_contacts.png
new file mode 100644
index 0000000..407d78c
--- /dev/null
+++ b/res/drawable-xxhdpi/empty_contacts.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png b/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png
new file mode 100644
index 0000000..7c256b5
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_remove.png b/res/drawable-xxhdpi/ic_remove.png
new file mode 100644
index 0000000..2722f23
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_remove.png
Binary files differ
diff --git a/res/drawable-xxhdpi/search_shadow.9.png b/res/drawable-xxhdpi/search_shadow.9.png
new file mode 100644
index 0000000..ff55620
--- /dev/null
+++ b/res/drawable-xxhdpi/search_shadow.9.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/empty_contacts.png b/res/drawable-xxxhdpi/empty_contacts.png
new file mode 100644
index 0000000..5893965
--- /dev/null
+++ b/res/drawable-xxxhdpi/empty_contacts.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png b/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png
new file mode 100644
index 0000000..6591ed4
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png
Binary files differ
diff --git a/res/drawable/rounded_corner.xml b/res/drawable/rounded_corner.xml
new file mode 100644
index 0000000..276fb30
--- /dev/null
+++ b/res/drawable/rounded_corner.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle" >
+    <solid android:color="@color/blocked_number_background" />
+    <corners android:radius="2dp" />
+</shape>
\ 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..e2b9403
--- /dev/null
+++ b/res/layout/blocked_number_fragment.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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/blocked_number_fragment"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical"
+              android:background="@color/blocked_number_background">
+
+    <ListView android:id="@id/android:list"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:divider="@null"
+              android:headerDividersEnabled="false" />
+
+</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..13ee7ba
--- /dev/null
+++ b/res/layout/blocked_number_header.xml
@@ -0,0 +1,179 @@
+<?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:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:focusable="false"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/blocked_numbers_disabled_for_emergency"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingTop="27dp"
+        android:paddingBottom="29dp"
+        android:paddingStart="@dimen/blocked_number_container_padding"
+        android:paddingEnd="44dp"
+        android:background="@color/blocked_number_disabled_emergency_background_color"
+        android:focusable="true"
+        android:orientation="vertical"
+        android:visibility="gone">
+
+        <TextView
+            style="@style/BlockedNumbersDescriptionTextStyle"
+            android:textStyle="bold"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/blocked_numbers_disabled_emergency_header_label"/>
+
+        <TextView
+            style="@style/BlockedNumbersDescriptionTextStyle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/blocked_numbers_disabled_emergency_desc"/>
+
+    </LinearLayout>
+
+    <android.support.v7.widget.CardView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        card_view:cardCornerRadius="0dp">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@android:color/white"
+            android:focusable="true"
+            android:orientation="vertical">
+
+            <LinearLayout
+                android:id="@+id/header_textview"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:padding="@dimen/blocked_number_container_padding"
+                android:background="@android:color/white"
+                android:focusable="true">
+
+                <TextView
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:textColor="@color/blocked_number_secondary_text_color"
+                    android:textSize="@dimen/blocked_number_settings_description_text_size"
+                    android:text="@string/blocked_number_header_message"/>
+            </LinearLayout>
+
+            <RelativeLayout
+                android:id="@+id/import_settings"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:visibility="gone">
+
+                <TextView
+                    android:id="@+id/import_description"
+                    style="@style/BlockedNumbersDescriptionTextStyle"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:paddingTop="11dp"
+                    android:paddingBottom="27dp"
+                    android:paddingStart="@dimen/blocked_number_container_padding"
+                    android:paddingEnd="@dimen/blocked_number_container_padding"
+                    android:text="@string/blocked_call_settings_import_description"
+                    android:textColor="@color/blocked_number_secondary_text_color"
+                    android:textSize="@dimen/blocked_number_settings_description_text_size"/>
+
+                <Button
+                    android:id="@+id/import_button"
+                    style="@style/ContactsFlatButtonStyle"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginEnd="@dimen/blocked_number_container_padding"
+                    android:layout_alignParentEnd="true"
+                    android:layout_below="@id/import_description"
+                    android:text="@string/blocked_call_settings_import_button"/>
+
+                <Button
+                    android:id="@+id/view_numbers_button"
+                    style="@style/ContactsFlatButtonStyle"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginEnd="8dp"
+                    android:layout_below="@id/import_description"
+                    android:layout_toStartOf="@id/import_button"
+                    android:text="@string/blocked_call_settings_view_numbers_button"/>
+
+                <View
+                    android:layout_width="match_parent"
+                    android:layout_height="1dp"
+                    android:layout_marginTop="8dp"
+                    android:layout_below="@id/import_button"
+                    android:background="@color/blocked_number_divider_line_color"/>
+
+            </RelativeLayout>
+
+            <LinearLayout
+                android:id="@+id/add_number_linear_layout"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingTop="@dimen/blocked_number_add_top_margin"
+                android:paddingBottom="@dimen/blocked_number_add_bottom_margin"
+                android:paddingStart="@dimen/blocked_number_horizontal_margin"
+                android:background="?android:attr/selectableItemBackground"
+                android:baselineAligned="false"
+                android:clickable="true"
+                android:contentDescription="@string/addBlockedNumber"
+                android:focusable="true"
+                android:gravity="center_vertical"
+                android:orientation="horizontal">
+
+                <ImageView
+                    android:id="@+id/add_number_icon"
+                    android:layout_width="@dimen/blocked_number_add_number_icon_size"
+                    android:layout_height="@dimen/blocked_number_add_number_icon_size"
+                    android:importantForAccessibility="no"/>
+                <LinearLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:layout_marginStart="@dimen/blocked_number_horizontal_margin"
+                    android:gravity="center_vertical"
+                    android:orientation="vertical">
+
+                    <TextView
+                        android:id="@+id/add_number_textview"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:includeFontPadding="false"
+                        android:text="@string/addBlockedNumber"
+                        android:textColor="@color/blocked_number_primary_text_color"
+                        android:textSize="@dimen/blocked_number_primary_text_size"/>
+                </LinearLayout>
+
+            </LinearLayout>
+
+            <View
+                android:id="@+id/blocked_number_list_divider"
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_marginStart="72dp"
+                android:background="@color/blocked_number_divider_line_color"/>
+
+        </LinearLayout>
+
+    </android.support.v7.widget.CardView>
+
+</LinearLayout>
diff --git a/res/layout/blocked_number_item.xml b/res/layout/blocked_number_item.xml
new file mode 100644
index 0000000..0d8b26f
--- /dev/null
+++ b/res/layout/blocked_number_item.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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:baselineAligned="false"
+    android:orientation="horizontal"
+    android:focusable="true"
+    android:gravity="center_vertical"
+    android:background="@android:color/white">
+
+    <QuickContactBadge
+        android:id="@+id/quick_contact_photo"
+        android:layout_width="@dimen/blocked_number_add_number_icon_size"
+        android:layout_height="@dimen/blocked_number_add_number_icon_size"
+        android:focusable="true"
+        android:layout_marginTop="@dimen/blocked_number_top_margin"
+        android:layout_marginBottom="@dimen/blocked_number_bottom_margin"/>
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:layout_weight="1"
+        android:orientation="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: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_settings_description_text_size"
+            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="24dp"
+        android:background="?android:attr/selectableItemBackgroundBorderless"
+        android:src="@drawable/ic_remove"
+        android:scaleType="center"
+        android:tint="@color/blocked_number_icon_tint"
+        android:contentDescription="@string/description_blocked_number_list_delete" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/blocked_numbers_activity.xml b/res/layout/blocked_numbers_activity.xml
new file mode 100644
index 0000000..6451496
--- /dev/null
+++ b/res/layout/blocked_numbers_activity.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             android:id="@+id/blocked_numbers_activity_container"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent">
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/empty_content_view.xml b/res/layout/empty_content_view.xml
new file mode 100644
index 0000000..97ac4c7
--- /dev/null
+++ b/res/layout/empty_content_view.xml
@@ -0,0 +1,54 @@
+<?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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <ImageView
+        android:id="@+id/emptyListViewImage"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:gravity="center_horizontal" />
+
+    <TextView
+        android:id="@+id/emptyListViewMessage"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal|top"
+        android:textSize="@dimen/empty_list_message_text_size"
+        android:textColor="@color/empty_list_text_color"
+        android:paddingRight="16dp"
+        android:paddingLeft="16dp"
+        android:paddingTop="8dp"
+        android:paddingBottom="8dp" />
+
+    <TextView
+        android:id="@+id/emptyListViewAction"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:layout_gravity="center_horizontal"
+        android:paddingRight="16dp"
+        android:paddingLeft="16dp"
+        android:paddingTop="8dp"
+        android:paddingBottom="8dp"
+        android:background="?android:attr/selectableItemBackground"
+        android:clickable="true"
+        style="@style/TextActionStyle" />
+
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="40dp" />
+
+</merge>
diff --git a/res/layout/search_edittext.xml b/res/layout/search_edittext.xml
new file mode 100644
index 0000000..2492ca9
--- /dev/null
+++ b/res/layout/search_edittext.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<view class="com.android.contacts.widget.SearchEditTextLayout"
+      xmlns:android="http://schemas.android.com/apk/res/android"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:id="@+id/search_view_container"
+      android:orientation="horizontal"
+      android:layout_marginTop="0dp"
+      android:layout_marginBottom="0dp"
+      android:layout_marginLeft="0dp"
+      android:layout_marginRight="0dp"
+      android:background="@drawable/rounded_corner"
+      android:elevation="3dp">
+
+    <include layout="@layout/search_bar_expanded" />
+
+</view>
diff --git a/res/layout/view_numbers_to_import_fragment.xml b/res/layout/view_numbers_to_import_fragment.xml
new file mode 100644
index 0000000..8f7331f
--- /dev/null
+++ b/res/layout/view_numbers_to_import_fragment.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="@color/blocked_number_background">
+
+    <ListView android:id="@id/android:list"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:divider="@null"
+        android:headerDividersEnabled="false" />
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom"
+        android:paddingTop="8dp"
+        android:paddingBottom="8dp"
+        android:background="@android:color/white">
+
+        <Button android:id="@+id/import_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentEnd="true"
+            android:layout_marginEnd="@dimen/blocked_number_container_padding"
+            android:text="@string/blocked_call_settings_import_button"
+            style="@style/ContactsFlatButtonStyle" />
+
+        <Button android:id="@+id/cancel_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/import_description"
+            android:layout_toLeftOf="@id/import_button"
+            android:text="@android:string/cancel"
+            style="@style/ContactsFlatButtonStyle" />
+
+    </RelativeLayout>
+
+</LinearLayout>
diff --git a/res/menu/people_options.xml b/res/menu/people_options.xml
index 6b604c1..af98f9d 100644
--- a/res/menu/people_options.xml
+++ b/res/menu/people_options.xml
@@ -34,6 +34,10 @@
         android:title="@string/menu_clear_frequents" />
 
     <item
+        android:id="@+id/menu_blocked_numbers"
+        android:title="@string/menu_blocked_numbers"/>
+
+    <item
         android:id="@+id/menu_accounts"
         android:title="@string/menu_accounts" />
 
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 0025a41..088c6ff 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -76,4 +76,18 @@
 
     <!-- Color of background of disabled link contacts button, 15% black. -->
     <color name="disabled_button_background">#26000000</color>
+
+    <!-- Color used in blocked numbers -->
+    <color name="blocked_number_background">#FFFFFF</color>
+    <color name="add_blocked_number_icon_color">#bdbdbd</color>
+    <color name="blocked_number_secondary_text_color">#636363</color>
+    <color name="blocked_number_header_color">@color/primary_color</color>
+    <color name="blocked_number_divider_line_color">#D8D8D8</color>
+    <color name="blocked_number_primary_text_color">#333333</color>
+    <color name="empty_list_text_color">#b2b2b2</color>
+    <color name="background_contacts_results">#f9f9f9</color>
+    <color name="blocked_number_block_color">#F44336</color>
+    <color name="contacts_snackbar_action_text_color">@color/primary_color</color>
+    <color name="blocked_number_disabled_emergency_background_color">#E0E0E0</color>
+    <color name="blocked_number_icon_tint">#616161</color>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 321c39b..cab8ac2 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -287,4 +287,21 @@
     <!-- Top margin for "Saving to" account header text field. -->
     <dimen name="compact_editor_account_header_top_margin">3dp</dimen>
 
+    <!-- Dimensions used in blocked numbers -->
+    <dimen name="blocked_number_settings_description_text_size">14sp</dimen>
+    <dimen name="blocked_number_container_padding">16dp</dimen>
+    <dimen name="blocked_number_top_margin">16dp</dimen>
+    <dimen name="blocked_number_bottom_margin">16dp</dimen>
+    <dimen name="blocked_number_add_top_margin">8dp</dimen>
+    <dimen name="blocked_number_add_bottom_margin">8dp</dimen>
+    <dimen name="blocked_number_horizontal_margin">16dp</dimen>
+    <dimen name="blocked_number_primary_text_size">16sp</dimen>
+    <dimen name="blocked_number_add_number_icon_size">40dp</dimen>
+    <dimen name="blocked_number_delete_icon_size">32dp</dimen>
+    <dimen name="empty_list_message_text_size">16sp</dimen>
+    <dimen name="call_log_action_height">48dp</dimen>
+    <dimen name="call_log_action_horizontal_padding">24dp</dimen>
+    <dimen name="search_list_padding_top">16dp</dimen>
+    <dimen name="blocked_number_search_text_size">14sp</dimen>
+    <dimen name="button_horizontal_padding">16dp</dimen>
 </resources>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index 7f6a51f..7a2ea0d 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -43,4 +43,7 @@
 
     <!-- An ID to be used for contents of a custom dialog so that its state be preserved -->
     <item type="id" name="custom_dialog_content" />
+
+    <!-- For blocked numbers -->
+    <item type="id" name="block_id" />
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c832e8f..56f19a1 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -913,4 +913,135 @@
     <!-- Text shown in the contacts app while the background process updates contacts after a locale change [CHAR LIMIT=150]-->
     <string name="locale_change_in_progress">Contact list is being updated to reflect the change of language.\n\nPlease wait...</string>
 
+    <!-- The blocked numbers activity title [CHAR LIMIT=50]-->
+    <string name="blocked_numbers_title">Blocked numbers</string>
+
+    <!-- Header message of blocked number activity [CHAR LIMIT=NONE] -->
+    <string name="blocked_number_header_message">
+        Calls and texts from these numbers will be blocked.
+    </string>
+
+    <!-- Button to bring up UI to add a number to the blocked call list. [CHAR LIMIT=40] -->
+    <string name="addBlockedNumber">Add number</string>
+
+    <!-- Shortcut item used to block a number directly from search. [CHAR LIMIT=25] -->
+    <string name="search_shortcut_block_number">Block number</string>
+
+    <!-- Hint displayed in add blocked number search box when there is no query typed.
+     [CHAR LIMIT=45] -->
+    <string name="block_number_search_hint">Add number or search contacts</string>
+
+    <!-- Confirmation dialog message for blocking a number. [CHAR LIMIT=NONE] -->
+    <string name="block_number_confirmation_message">
+        Calls and texts from this number will be blocked.
+    </string>
+
+    <!-- Confirmation dialog for unblocking a number. [CHAR LIMIT=NONE] -->
+    <string name="unblock_number_confirmation_title">Unblock
+        <xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g>?</string>
+
+    <!-- Unblock number alert dialog button [CHAR LIMIT=32] -->
+    <string name="unblock_number_ok">UNBLOCK</string>
+
+    <!-- Confirmation dialog title for blocking a number. [CHAR LIMIT=NONE] -->
+    <string name="block_number_confirmation_title">Block
+        <xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g>?</string>
+
+    <!-- Block number alert dialog button [CHAR LIMIT=32] -->
+    <string name="block_number_ok">BLOCK</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> blocked</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>
+        unblocked</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>
+
+    <!-- Label for a section describing that call blocking is temporarily disabled because an
+     emergency call was made. [CHAR LIMIT=50] -->
+    <string name="blocked_numbers_disabled_emergency_header_label">
+        Call blocking temporarily off
+    </string>
+
+    <!-- Description that call blocking is temporarily disabled because the user called an
+     emergency number, and explains that call blocking will be re-enabled after a buffer
+     period has passed. [CHAR LIMIT=NONE] -->
+    <string name="blocked_numbers_disabled_emergency_desc">
+        Call blocking has been disabled because you contacted emergency services from this phone
+        within the last 48 hours. It will be automatically reenabled once the 48 hour period
+        expires.
+    </string>
+
+    <!-- Text informing the user they have previously marked contacts to be sent to voicemail.
+     This will be followed by two buttons, 1) to view who is marked to be sent to voicemail
+     and 2) importing these settings to block list. [CHAR LIMIT=NONE] -->
+    <string name="blocked_call_settings_import_description">
+        You previously marked some contacts to be automatically sent to voicemail. Import those numbers here to block both calls and texts.
+    </string>
+
+    <!-- Label for button to import settings for sending contacts to voicemail into block list. [CHAR_LIMIT=20] -->
+    <string name="blocked_call_settings_import_button">Import</string>
+
+    <!-- Label for button to view numbers of contacts previous marked to be sent to voicemail.
+     [CHAR_LIMIT=20] -->
+    <string name="blocked_call_settings_view_numbers_button">View Numbers</string>
+
+    <!-- Button to bring up UI to add a number to the blocked call list. [CHAR LIMIT=40] -->
+    <string name="addBlockedNumber">Add number</string>
+
+    <!-- Title of notification telling the user that call blocking has been temporarily disabled.
+         [CHAR LIMIT=56] -->
+    <string name="call_blocking_disabled_notification_title">
+        Call blocking disabled for 48 hours
+    </string>
+
+    <!-- Text for notification which provides the reason that call blocking has been temporarily
+         disabled. Namely, we disable call blocking after an emergency call in case of return
+         phone calls made by emergency services. [CHAR LIMIT=64] -->
+    <string name="call_blocking_disabled_notification_text">
+        Disabled because an emergency call was made.
+    </string>
+
+    <!-- Text for undo button in snackbar for blocking/unblocking number. [CHAR LIMIT=10] -->
+    <string name="block_number_undo">UNDO</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>
+
+    <!-- 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>
+
+    <!-- String describing the button to access the contact details for a name or number.
+
+    Note: AccessibilityServices use this attribute to announce what the view represents.
+          This is especially valuable for views without textual representation like ImageView.
+-->
+    <string name="description_contact_details">Contact details for <xliff:g id="nameOrNumber">%1$s</xliff:g></string>
+
+    <!-- Error toast message for when send to voicemail import fails. [CHAR LIMIT=40] -->
+    <string name="send_to_voicemail_import_failed">Import failed</string>
+
+    <!-- Label for fragment to import numbers from contacts marked as send to voicemail.
+     [CHAR_LIMIT=30] -->
+    <string name="import_send_to_voicemail_numbers_label">Import numbers</string>
+
+    <!-- Shown as a prompt to turn on contacts permissions to allow contact search [CHAR LIMIT=NONE]-->
+    <string name="permission_no_search">To search your contacts, turn on the Contacts permissions.</string>
+
+    <!-- The label of the button used to turn on a single permission [CHAR LIMIT=30]-->
+    <string name="permission_single_turn_on">Turn on</string>
+
+    <!-- The menu item to open blocked numbers activity [CHAR LIMIT=60]-->
+    <string name="menu_blocked_numbers">Blocked numbers</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 7df52c0..db1b5d8 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -453,4 +453,51 @@
         <item name="android:textColor">#363636</item>
         <item name="android:fontFamily">sans-serif</item>
     </style>
+
+    <style name="TextActionStyle">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">@dimen/call_log_action_height</item>
+        <item name="android:gravity">end|center_vertical</item>
+        <item name="android:paddingStart">@dimen/call_log_action_horizontal_padding</item>
+        <item name="android:paddingEnd">@dimen/call_log_action_horizontal_padding</item>
+        <item name="android:textColor">@color/primary_color</item>
+        <item name="android:fontFamily">"sans-serif-medium"</item>
+        <item name="android:focusable">true</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:textAllCaps">true</item>
+    </style>
+
+    <style name="BlockedNumbersDescriptionTextStyle">
+        <item name="android:lineSpacingMultiplier">1.43</item>
+        <item name="android:paddingTop">8dp</item>
+        <item name="android:paddingBottom">8dp</item>
+        <item name="android:textSize">@dimen/blocked_number_settings_description_text_size</item>
+    </style>
+
+    <style name="ContactsFlatButtonStyle" parent="@style/Widget.AppCompat.Button.Colored">
+        <item name="android:background">?android:attr/selectableItemBackground</item>
+        <item name="android:paddingEnd">@dimen/button_horizontal_padding</item>
+        <item name="android:paddingStart">@dimen/button_horizontal_padding</item>
+        <item name="android:textColor">@color/primary_color</item>
+        <item name="background">?android:attr/selectableItemBackground</item>
+        <item name="paddingEnd">@dimen/button_horizontal_padding</item>
+        <item name="paddingStart">@dimen/button_horizontal_padding</item>
+    </style>
+
+    <style name="BlockedNumbersStyle" parent="@style/PeopleThemeAppCompat">
+        <item name="android:actionBarStyle">@style/BlockedNumbersStyleActionBarStyle</item>
+        <item name="actionBarStyle">@style/BlockedNumbersStyleActionBarStyle</item>
+    </style>
+
+    <style name="BlockedNumbersStyleActionBarStyle"
+           parent="@style/ContactsActionBarStyleAppCompat">
+        <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+        <item name="android:displayOptions"></item>
+        <item name="displayOptions"></item>
+        <!-- Override ActionBar title offset to keep search box aligned left -->
+        <item name="android:contentInsetStart">0dp</item>
+        <item name="contentInsetStart">0dp</item>
+        <item name="android:contentInsetEnd">0dp</item>
+        <item name="contentInsetEnd">0dp</item>
+    </style>
 </resources>
diff --git a/src/com/android/contacts/activities/BlockedNumbersActivity.java b/src/com/android/contacts/activities/BlockedNumbersActivity.java
new file mode 100644
index 0000000..daaeb20
--- /dev/null
+++ b/src/com/android/contacts/activities/BlockedNumbersActivity.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2016 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.contacts.activities;
+
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.view.MenuItem;
+
+import com.android.contacts.AppCompatContactsActivity;
+import com.android.contacts.R;
+import com.android.contacts.callblocking.BlockedNumbersFragment;
+import com.android.contacts.callblocking.SearchFragment;
+import com.android.contacts.callblocking.ViewNumbersToImportFragment;
+
+public class BlockedNumbersActivity extends AppCompatContactsActivity
+        implements SearchFragment.HostInterface {
+    private static final String TAG_BLOCKED_MANAGEMENT_FRAGMENT = "blocked_management";
+    private static final String TAG_BLOCKED_SEARCH_FRAGMENT = "blocked_search";
+    private static final String TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT = "view_numbers_to_import";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.blocked_numbers_activity);
+
+        final ActionBar actionBar = getSupportActionBar();
+
+        actionBar.setDisplayShowHomeEnabled(true);
+        actionBar.setDisplayHomeAsUpEnabled(true);
+
+        // If savedInstanceState != null, the activity will automatically restore the last fragment.
+        if (savedInstanceState == null) {
+            showManagementUi();
+        }
+    }
+
+    /**
+     * Shows fragment with the list of currently blocked numbers and settings related to blocking.
+     */
+    public void showManagementUi() {
+        BlockedNumbersFragment fragment = (BlockedNumbersFragment) getFragmentManager()
+                .findFragmentByTag(TAG_BLOCKED_MANAGEMENT_FRAGMENT);
+        if (fragment == null) {
+            fragment = new BlockedNumbersFragment();
+        }
+
+        getFragmentManager().beginTransaction()
+                .replace(R.id.blocked_numbers_activity_container, fragment,
+                        TAG_BLOCKED_MANAGEMENT_FRAGMENT)
+                .commit();
+    }
+
+    /**
+     * Shows fragment with search UI for browsing/finding numbers to block.
+     */
+    public void showSearchUi() {
+        SearchFragment fragment = (SearchFragment) getFragmentManager()
+                .findFragmentByTag(TAG_BLOCKED_SEARCH_FRAGMENT);
+        if (fragment == null) {
+            fragment = new SearchFragment();
+            fragment.setHasOptionsMenu(false);
+            fragment.setShowEmptyListForNullQuery(true);
+            fragment.setDirectorySearchEnabled(false);
+        }
+
+        getFragmentManager().beginTransaction()
+                .replace(R.id.blocked_numbers_activity_container, fragment,
+                        TAG_BLOCKED_SEARCH_FRAGMENT)
+                .addToBackStack(null)
+                .commit();
+    }
+
+    /**
+     * Shows fragment with UI to preview the numbers of contacts currently marked as
+     * send-to-voicemail in Contacts. These numbers can be imported into blocked number list.
+     */
+    public void showNumbersToImportPreviewUi() {
+        ViewNumbersToImportFragment fragment = (ViewNumbersToImportFragment) getFragmentManager()
+                .findFragmentByTag(TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT);
+        if (fragment == null) {
+            fragment = new ViewNumbersToImportFragment();
+        }
+
+        getFragmentManager().beginTransaction()
+                .replace(R.id.blocked_numbers_activity_container, fragment,
+                        TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT)
+                .addToBackStack(null)
+                .commit();
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            onBackPressed();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onBackPressed() {
+        // TODO: Achieve back navigation without overriding onBackPressed.
+        if (getFragmentManager().getBackStackEntryCount() > 0) {
+            getFragmentManager().popBackStack();
+        } else {
+            super.onBackPressed();
+        }
+    }
+
+    @Override
+    public boolean isActionBarShowing() {
+        return true;
+    }
+
+    @Override
+    public boolean isDialpadShown() {
+        return false;
+    }
+
+    @Override
+    public int getDialpadHeight() {
+        return 0;
+    }
+
+    @Override
+    public int getActionBarHideOffset() {
+        return 0;
+    }
+
+    @Override
+    public int getActionBarHeight() {
+        return 0;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 7f06629..9806c8c 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -27,6 +27,7 @@
 import android.content.pm.ResolveInfo;
 import android.graphics.Rect;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.os.UserManager;
@@ -92,6 +93,7 @@
 import com.android.contacts.quickcontact.QuickContactActivity;
 import com.android.contacts.util.AccountPromptUtils;
 import com.android.contacts.common.util.Constants;
+import com.android.contacts.util.PhoneCapabilityTester;
 import com.android.contacts.util.DialogManager;
 import com.android.contactsbind.HelpUtils;
 
@@ -1119,9 +1121,15 @@
             helpMenu.setVisible(HelpUtils.isHelpAndFeedbackAvailable());
         }
         final boolean showMiscOptions = !isSearchOrSelectionMode;
+        //TODO use ContactsUtils.FLAG_N_FEATURE
+        final boolean isBlockedNumbersCompatible =
+                Build.VERSION.SDK_INT > Build.VERSION_CODES.M;
+        final boolean showBlockedNumbers = PhoneCapabilityTester.isPhone(this) &&
+                isBlockedNumbersCompatible;
         makeMenuItemVisible(menu, R.id.menu_search, showMiscOptions);
         makeMenuItemVisible(menu, R.id.menu_import_export, showMiscOptions);
         makeMenuItemVisible(menu, R.id.menu_accounts, showMiscOptions);
+        makeMenuItemVisible(menu, R.id.menu_blocked_numbers, showMiscOptions && showBlockedNumbers);
         makeMenuItemVisible(menu, R.id.menu_settings,
                 showMiscOptions && !ContactsPreferenceActivity.isEmpty(this));
 
@@ -1239,6 +1247,12 @@
                 ImplicitIntentsUtil.startActivityInAppIfPossible(this, intent);
                 return true;
             }
+            case R.id.menu_blocked_numbers: {
+                final Intent intent = new Intent("android.intent.action.EDIT");
+                intent.setType("blocked_numbers/*");
+                ImplicitIntentsUtil.startActivityInApp(this, intent);
+                return true;
+            }
             case R.id.export_database: {
                 final Intent intent = new Intent("com.android.providers.contacts.DUMP_DATABASE");
                 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
diff --git a/src/com/android/contacts/callblocking/BlockNumberDialogFragment.java b/src/com/android/contacts/callblocking/BlockNumberDialogFragment.java
new file mode 100644
index 0000000..80ad1f7
--- /dev/null
+++ b/src/com/android/contacts/callblocking/BlockNumberDialogFragment.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.design.widget.Snackbar;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Toast;
+
+import com.android.contacts.R;
+import com.android.contacts.callblocking.FilteredNumberAsyncQueryHandler.OnBlockNumberListener;
+import com.android.contacts.callblocking.FilteredNumberAsyncQueryHandler.OnUnblockNumberListener;
+import com.android.contacts.common.util.ContactDisplayUtils;
+
+/**
+ * Fragment for confirming and enacting blocking/unblocking a number. Also invokes snackbar
+ * providing undo functionality but not checks whether visual voicemail is enabled.
+ */
+public class BlockNumberDialogFragment extends DialogFragment {
+
+    /**
+     * Use a callback interface to update UI after success/undo. Favor this approach over other
+     * more standard paradigms because of the variety of scenarios in which the DialogFragment
+     * can be invoked (by an Activity, by a fragment, by an adapter, by an adapter list item).
+     * Because of this, we do NOT support retaining state on rotation, and will dismiss the dialog
+     * upon rotation instead.
+     */
+    public interface Callback {
+        /**
+         * Called when a number is successfully added to the set of filtered numbers
+         */
+        void onFilterNumberSuccess();
+
+        /**
+         * Called when a number is successfully removed from the set of filtered numbers
+         */
+        void onUnfilterNumberSuccess();
+
+        /**
+         * Called when the action of filtering or unfiltering a number is undone
+         */
+        void onChangeFilteredNumberUndo();
+    }
+
+    private static final String BLOCK_DIALOG_FRAGMENT = "BlockNumberDialog";
+
+    private static final String ARG_BLOCK_ID = "argBlockId";
+    private static final String ARG_NUMBER = "argNumber";
+    private static final String ARG_COUNTRY_ISO = "argCountryIso";
+    private static final String ARG_DISPLAY_NUMBER = "argDisplayNumber";
+    private static final String ARG_PARENT_VIEW_ID = "parentViewId";
+
+    private String mNumber;
+    private String mDisplayNumber;
+    private String mCountryIso;
+
+    private FilteredNumberAsyncQueryHandler mHandler;
+    private View mParentView;
+    private Callback mCallback;
+
+    public static void show(
+            Integer blockId,
+            String number,
+            String countryIso,
+            String displayNumber,
+            Integer parentViewId,
+            FragmentManager fragmentManager,
+            Callback callback) {
+        final BlockNumberDialogFragment newFragment = BlockNumberDialogFragment.newInstance(
+                blockId, number, countryIso, displayNumber, parentViewId);
+
+        newFragment.setCallback(callback);
+        newFragment.show(fragmentManager, BlockNumberDialogFragment.BLOCK_DIALOG_FRAGMENT);
+    }
+
+    private static BlockNumberDialogFragment newInstance(
+            Integer blockId,
+            String number,
+            String countryIso,
+            String displayNumber,
+            Integer parentViewId) {
+        final BlockNumberDialogFragment fragment = new BlockNumberDialogFragment();
+        final Bundle args = new Bundle();
+        if (blockId != null) {
+            args.putInt(ARG_BLOCK_ID, blockId.intValue());
+        }
+        if (parentViewId != null) {
+            args.putInt(ARG_PARENT_VIEW_ID, parentViewId.intValue());
+        }
+        args.putString(ARG_NUMBER, number);
+        args.putString(ARG_COUNTRY_ISO, countryIso);
+        args.putString(ARG_DISPLAY_NUMBER, displayNumber);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Override
+    public Context getContext() {
+        return getActivity();
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        super.onCreateDialog(savedInstanceState);
+        final boolean isBlocked = getArguments().containsKey(ARG_BLOCK_ID);
+
+        mNumber = getArguments().getString(ARG_NUMBER);
+        mDisplayNumber = getArguments().getString(ARG_DISPLAY_NUMBER);
+        mCountryIso = getArguments().getString(ARG_COUNTRY_ISO);
+
+        if (TextUtils.isEmpty(mDisplayNumber)) {
+            mDisplayNumber = mNumber;
+        }
+
+        mHandler = new FilteredNumberAsyncQueryHandler(getContext().getContentResolver());
+        /**
+         * Choose not to update VoicemailEnabledChecker, as checks should already been done in
+         * all current use cases.
+         */
+        mParentView = getActivity().findViewById(getArguments().getInt(ARG_PARENT_VIEW_ID));
+
+        String okText;
+        CharSequence message;
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        if (isBlocked) {
+            okText = getString(R.string.unblock_number_ok);
+            message = ContactDisplayUtils.getTtsSpannedPhoneNumber(getResources(),
+                    R.string.unblock_number_confirmation_title,
+                    mDisplayNumber);
+        } else {
+            builder.setTitle(ContactDisplayUtils.getTtsSpannedPhoneNumber(getResources(),
+                    R.string.block_number_confirmation_title,
+                    mDisplayNumber));
+            okText = getString(R.string.block_number_ok);
+            message = getString(R.string.block_number_confirmation_message);
+        }
+
+        builder.setMessage(message).setNegativeButton(android.R.string.cancel, null);
+        builder.setPositiveButton(okText, new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int id) {
+                if (isBlocked) {
+                    unblockNumber();
+                } else {
+                    blockNumber();
+                        }
+                    }
+                });
+        return builder.create();
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        if (!FilteredNumbersUtil.canBlockNumber(getActivity(), mNumber, mCountryIso)) {
+            dismiss();
+            Toast.makeText(getContext(),
+                    ContactDisplayUtils.getTtsSpannedPhoneNumber(
+                            getResources(), R.string.invalidNumber, mDisplayNumber),
+                    Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    @Override
+    public void onPause() {
+        // TODO: avoid dismissing the dialog fragment
+        // Dismiss on rotation.
+        dismiss();
+        mCallback = null;
+
+        super.onPause();
+    }
+
+    public void setCallback(Callback callback) {
+        mCallback = callback;
+    }
+
+    private CharSequence getBlockedMessage() {
+        return ContactDisplayUtils.getTtsSpannedPhoneNumber(getResources(),
+                R.string.snackbar_number_blocked, mDisplayNumber);
+    }
+
+    private CharSequence getUnblockedMessage() {
+        return ContactDisplayUtils.getTtsSpannedPhoneNumber(getResources(),
+                R.string.snackbar_number_unblocked, mDisplayNumber);
+    }
+
+    private int getActionTextColor() {
+        return getContext().getResources().getColor(R.color.contacts_snackbar_action_text_color);
+    }
+
+    private void blockNumber() {
+        final CharSequence message = getBlockedMessage();
+        final CharSequence undoMessage = getUnblockedMessage();
+        final Callback callback = mCallback;
+        final int actionTextColor = getActionTextColor();
+
+        final OnUnblockNumberListener onUndoListener = new OnUnblockNumberListener() {
+            @Override
+            public void onUnblockComplete(int rows, ContentValues values) {
+                Snackbar.make(mParentView, undoMessage, Snackbar.LENGTH_LONG).show();
+                if (callback != null) {
+                    callback.onChangeFilteredNumberUndo();
+                }
+            }
+        };
+
+        final OnBlockNumberListener onBlockNumberListener = new OnBlockNumberListener() {
+            @Override
+            public void onBlockComplete(final Uri uri) {
+                final View.OnClickListener undoListener = new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        // Delete the newly created row on 'undo'.
+                        mHandler.unblock(onUndoListener, uri);
+                    }
+                };
+
+                Snackbar.make(mParentView, message, Snackbar.LENGTH_LONG)
+                        .setAction(R.string.block_number_undo, undoListener)
+                        .setActionTextColor(actionTextColor)
+                        .show();
+
+                if (callback != null) {
+                    callback.onFilterNumberSuccess();
+                }
+
+                // Since Dialer shows notification, Contacts will not do that again.
+            }
+        };
+
+        mHandler.blockNumber(
+                onBlockNumberListener,
+                mNumber,
+                mCountryIso);
+    }
+
+    private void unblockNumber() {
+        final CharSequence message = getUnblockedMessage();
+        final CharSequence undoMessage = getBlockedMessage();
+        final Callback callback = mCallback;
+        final int actionTextColor = getActionTextColor();
+
+        final OnBlockNumberListener onUndoListener = new OnBlockNumberListener() {
+            @Override
+            public void onBlockComplete(final Uri uri) {
+                Snackbar.make(mParentView, undoMessage, Snackbar.LENGTH_LONG).show();
+                if (callback != null) {
+                    callback.onChangeFilteredNumberUndo();
+                }
+            }
+        };
+
+        mHandler.unblock(new OnUnblockNumberListener() {
+            @Override
+            public void onUnblockComplete(int rows, final ContentValues values) {
+                final View.OnClickListener undoListener = new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        // Re-insert the row on 'undo', with a new ID.
+                        mHandler.blockNumber(onUndoListener, values);
+                    }
+                };
+
+                Snackbar.make(mParentView, message, Snackbar.LENGTH_LONG)
+                        .setAction(R.string.block_number_undo, undoListener)
+                        .setActionTextColor(actionTextColor)
+                        .show();
+
+                if (callback != null) {
+                    callback.onUnfilterNumberSuccess();
+                }
+            }
+        }, getArguments().getInt(ARG_BLOCK_ID));
+    }
+}
diff --git a/src/com/android/contacts/callblocking/BlockedNumbersAdapter.java b/src/com/android/contacts/callblocking/BlockedNumbersAdapter.java
new file mode 100644
index 0000000..2e31f71
--- /dev/null
+++ b/src/com/android/contacts/callblocking/BlockedNumbersAdapter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.app.FragmentManager;
+import android.content.Context;
+import android.database.Cursor;
+import android.telephony.PhoneNumberUtils;
+import android.view.View;
+
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.R;
+import com.android.contacts.callblocking.FilteredNumberContract.FilteredNumberColumns;
+
+public class BlockedNumbersAdapter extends NumbersAdapter {
+    private BlockedNumbersAdapter(
+            Context context,
+            FragmentManager fragmentManager,
+            ContactInfoHelper contactInfoHelper,
+            ContactPhotoManager contactPhotoManager) {
+        super(context, fragmentManager, contactInfoHelper, contactPhotoManager);
+    }
+
+    public static BlockedNumbersAdapter newBlockedNumbersAdapter(
+            Context context, FragmentManager fragmentManager) {
+        return new BlockedNumbersAdapter(
+                context,
+                fragmentManager,
+                new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)),
+                ContactPhotoManager.getInstance(context));
+    }
+
+    @Override
+    public void bindView(View view, final Context context, Cursor cursor) {
+        super.bindView(view, context, cursor);
+        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 View deleteButton = view.findViewById(R.id.delete_button);
+        deleteButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                BlockNumberDialogFragment.show(
+                        id,
+                        number,
+                        countryIso,
+                        PhoneNumberUtils.formatNumber(number, countryIso),
+                        R.id.blocked_numbers_activity_container,
+                        getFragmentManager(),
+                        new BlockNumberDialogFragment.Callback() {
+                            @Override
+                            public void onFilterNumberSuccess() {}
+
+                            @Override
+                            public void onUnfilterNumberSuccess() {}
+
+                            @Override
+                            public void onChangeFilteredNumberUndo() {}
+                        });
+            }
+        });
+
+        updateView(view, number, countryIso);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        // Always return false, so that the header with blocking-related options always shows.
+        return false;
+    }
+}
diff --git a/src/com/android/contacts/callblocking/BlockedNumbersFragment.java b/src/com/android/contacts/callblocking/BlockedNumbersFragment.java
new file mode 100644
index 0000000..fbdfa00
--- /dev/null
+++ b/src/com/android/contacts/callblocking/BlockedNumbersFragment.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.app.ListFragment;
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import com.android.contacts.R;
+import com.android.contacts.activities.BlockedNumbersActivity;
+import com.android.contacts.callblocking.FilteredNumbersUtil.CheckForSendToVoicemailContactListener;
+import com.android.contacts.callblocking.FilteredNumbersUtil.ImportSendToVoicemailContactsListener;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+
+/**
+ * This class is copied from Dialer, but we don't check whether visual voicemail is enabled here.
+  */
+public class BlockedNumbersFragment extends ListFragment
+        implements LoaderManager.LoaderCallbacks<Cursor>, View.OnClickListener {
+
+    private static final char ADD_BLOCKED_NUMBER_ICON_LETTER = '+';
+
+    private BlockedNumbersAdapter mAdapter;
+
+    private View mImportSettings;
+    private View mBlockedNumbersDisabledForEmergency;
+    private View mBlockedNumberListDivider;
+    private View mHeaderTextView;
+
+    @Override
+    public Context getContext() {
+        return getActivity();
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        LayoutInflater inflater =
+                (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        getListView().addHeaderView(inflater.inflate(R.layout.blocked_number_header, null));
+        //replace the icon for add number with LetterTileDrawable(), so it will have identical style
+        ImageView addNumberIcon = (ImageView) getActivity().findViewById(R.id.add_number_icon);
+        LetterTileDrawable drawable = new LetterTileDrawable(getResources());
+        drawable.setLetter(ADD_BLOCKED_NUMBER_ICON_LETTER);
+        drawable.setColor(ActivityCompat.getColor(getActivity(),
+                R.color.add_blocked_number_icon_color));
+        drawable.setIsCircular(true);
+        addNumberIcon.setImageDrawable(drawable);
+
+        if (mAdapter == null) {
+            mAdapter = BlockedNumbersAdapter.newBlockedNumbersAdapter(
+                    getContext(), getActivity().getFragmentManager());
+        }
+        setListAdapter(mAdapter);
+
+        mHeaderTextView = getListView().findViewById(R.id.header_textview);
+        mImportSettings = getListView().findViewById(R.id.import_settings);
+        mBlockedNumbersDisabledForEmergency =
+                getListView().findViewById(R.id.blocked_numbers_disabled_for_emergency);
+        mBlockedNumberListDivider = getActivity().findViewById(R.id.blocked_number_list_divider);
+        getListView().findViewById(R.id.import_button).setOnClickListener(this);
+        getListView().findViewById(R.id.view_numbers_button).setOnClickListener(this);
+        getListView().findViewById(R.id.add_number_linear_layout).setOnClickListener(this);
+    }
+
+    @Override
+    public void onDestroy() {
+        setListAdapter(null);
+        super.onDestroy();
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        getLoaderManager().initLoader(0, null, this);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+        ColorDrawable backgroundDrawable = new ColorDrawable(
+                ActivityCompat.getColor(getActivity(), R.color.primary_color));
+        actionBar.setBackgroundDrawable(backgroundDrawable);
+        actionBar.setDisplayShowCustomEnabled(false);
+        actionBar.setDisplayHomeAsUpEnabled(true);
+        actionBar.setDisplayShowHomeEnabled(true);
+        actionBar.setDisplayShowTitleEnabled(true);
+        actionBar.setTitle(R.string.blocked_numbers_title);
+
+        FilteredNumbersUtil.checkForSendToVoicemailContact(
+            getActivity(), new CheckForSendToVoicemailContactListener() {
+                @Override
+                public void onComplete(boolean hasSendToVoicemailContact) {
+                    mImportSettings.setVisibility(
+                            hasSendToVoicemailContact ? View.VISIBLE : View.GONE);
+                    mHeaderTextView.setVisibility(
+                            mImportSettings.getVisibility() == View.GONE ?
+                                    View.VISIBLE : View.GONE);
+                }
+            });
+
+        // Visibility of mBlockedNumbersDisabledForEmergency will always be GONE for now, until
+        // we could check recent emergency call from framework.
+    }
+
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.blocked_number_fragment, container, false);
+    }
+
+    @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);
+        if (data.getCount() == 0) {
+            mBlockedNumberListDivider.setVisibility(View.INVISIBLE);
+        } else {
+            mBlockedNumberListDivider.setVisibility(View.VISIBLE);
+        }
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+        mAdapter.swapCursor(null);
+    }
+
+    @Override
+    public void onClick(View view) {
+        BlockedNumbersActivity activity = (BlockedNumbersActivity) getActivity();
+        if (activity == null) {
+            return;
+        }
+
+        switch (view.getId()) {
+            case R.id.add_number_linear_layout:
+                activity.showSearchUi();
+                break;
+            case R.id.view_numbers_button:
+                activity.showNumbersToImportPreviewUi();
+                break;
+            case R.id.import_button:
+                FilteredNumbersUtil.importSendToVoicemailContacts(activity,
+                        new ImportSendToVoicemailContactsListener() {
+                            @Override
+                            public void onImportComplete() {
+                                mImportSettings.setVisibility(View.GONE);
+                                mHeaderTextView.setVisibility(View.VISIBLE);
+                            }
+                        });
+                break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/callblocking/ContactInfo.java b/src/com/android/contacts/callblocking/ContactInfo.java
new file mode 100644
index 0000000..a96cfb7
--- /dev/null
+++ b/src/com/android/contacts/callblocking/ContactInfo.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.util.UriUtils;
+
+import com.google.common.base.Objects;
+
+/**
+ * Information for a contact as needed by blocked numbers.
+ */
+final public class ContactInfo {
+    public Uri lookupUri;
+
+    /**
+     * Contact lookup key.  Note this may be a lookup key for a corp contact, in which case
+     * "lookup by lookup key" doesn't work on the personal profile.
+     */
+    public String lookupKey;
+    public String name;
+    public String nameAlternative;
+    public int type;
+    public String label;
+    public String number;
+    public String formattedNumber;
+    public String normalizedNumber;
+    /** The photo for the contact, if available. */
+    public long photoId;
+    /** The high-res photo for the contact, if available. */
+    public Uri photoUri;
+    public boolean isBadData;
+    public String objectId;
+    public @UserType long userType;
+
+    public static ContactInfo EMPTY = new ContactInfo();
+
+    public int sourceType = 0;
+
+    @Override
+    public int hashCode() {
+        // Uses only name and contactUri to determine hashcode.
+        // This should be sufficient to have a reasonable distribution of hash codes.
+        // Moreover, there should be no two people with the same lookupUri.
+        return Objects.hashCode(lookupUri, name);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (Objects.equal(this, obj)) return true;
+        if (obj == null) return false;
+        if (obj instanceof ContactInfo) {
+            ContactInfo other = (ContactInfo) obj;
+            return Objects.equal(lookupUri, other.lookupUri)
+                    && TextUtils.equals(name, other.name)
+                    && TextUtils.equals(nameAlternative, other.nameAlternative)
+                    && Objects.equal(type, other.type)
+                    && TextUtils.equals(label, other.label)
+                    && TextUtils.equals(number, other.number)
+                    && TextUtils.equals(formattedNumber, other.formattedNumber)
+                    && TextUtils.equals(normalizedNumber, other.normalizedNumber)
+                    && Objects.equal(photoId, other.photoId)
+                    && Objects.equal(photoUri, other.photoUri)
+                    && TextUtils.equals(objectId, other.objectId)
+                    && Objects.equal(userType, other.userType);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this).add("lookupUri", lookupUri).add("name", name)
+                .add("nameAlternative", nameAlternative)
+                .add("type", type).add("label", label)
+                .add("number", number).add("formattedNumber",formattedNumber)
+                .add("normalizedNumber", normalizedNumber).add("photoId", photoId)
+                .add("photoUri", photoUri).add("objectId", objectId).toString();
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/callblocking/ContactInfoHelper.java b/src/com/android/contacts/callblocking/ContactInfoHelper.java
new file mode 100644
index 0000000..de31e37
--- /dev/null
+++ b/src/com/android/contacts/callblocking/ContactInfoHelper.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.PhoneLookup;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.util.Constants;
+import com.android.contacts.common.util.PermissionsUtil;
+import com.android.contacts.common.util.PhoneNumberHelper;
+import com.android.contacts.common.util.UriUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Utility class to look up the contact information for a given number.
+ */
+public class ContactInfoHelper {
+    private final Context mContext;
+    private final String mCurrentCountryIso;
+
+    /**
+     * The queries to look up the {@link ContactInfo} for a given number in the Call Log.
+     */
+    private static final class PhoneQuery {
+
+        /**
+         * Projection to look up the ContactInfo. Does not include DISPLAY_NAME_ALTERNATIVE as that
+         * column isn't available in ContactsCommon.PhoneLookup
+         */
+        public static final String[] PHONE_LOOKUP_PROJECTION = new String[] {
+                PhoneLookup._ID,
+                PhoneLookup.DISPLAY_NAME,
+                PhoneLookup.TYPE,
+                PhoneLookup.LABEL,
+                PhoneLookup.NUMBER,
+                PhoneLookup.NORMALIZED_NUMBER,
+                PhoneLookup.PHOTO_ID,
+                PhoneLookup.LOOKUP_KEY,
+                PhoneLookup.PHOTO_URI};
+
+        public static final int PERSON_ID = 0;
+        public static final int NAME = 1;
+        public static final int PHONE_TYPE = 2;
+        public static final int LABEL = 3;
+        public static final int MATCHED_NUMBER = 4;
+        public static final int NORMALIZED_NUMBER = 5;
+        public static final int PHOTO_ID = 6;
+        public static final int LOOKUP_KEY = 7;
+        public static final int PHOTO_URI = 8;
+    }
+
+    private static final class NameAlternativeQuery {
+        /**
+         * Projection to look up a contact's DISPLAY_NAME_ALTERNATIVE
+         */
+        public static final String[] DISPLAY_NAME_PROJECTION = new String[] {
+                Contacts.DISPLAY_NAME_ALTERNATIVE,
+        };
+
+        public static final int NAME = 0;
+    }
+
+    public ContactInfoHelper(Context context, String currentCountryIso) {
+        mContext = context;
+        mCurrentCountryIso = currentCountryIso;
+    }
+
+    /**
+     * Returns the contact information for the given number.
+     * <p>
+     * If the number does not match any contact, returns a contact info containing only the number
+     * and the formatted number.
+     * <p>
+     * If an error occurs during the lookup, it returns null.
+     *
+     * @param number the number to look up
+     * @param countryIso the country associated with this number
+     */
+    @Nullable
+    public ContactInfo lookupNumber(String number, String countryIso) {
+        if (TextUtils.isEmpty(number)) {
+            return null;
+        }
+
+        ContactInfo info;
+
+        if (PhoneNumberHelper.isUriNumber(number)) {
+            // The number is a SIP address..
+            info = lookupContactFromUri(getContactInfoLookupUri(number));
+            if (info == null || info == ContactInfo.EMPTY) {
+                // If lookup failed, check if the "username" of the SIP address is a phone number.
+                String username = PhoneNumberHelper.getUsernameFromUriNumber(number);
+                if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
+                    info = queryContactInfoForPhoneNumber(username, countryIso);
+                }
+            }
+        } else {
+            // Look for a contact that has the given phone number.
+            info = queryContactInfoForPhoneNumber(number, countryIso);
+        }
+
+        final ContactInfo updatedInfo;
+        if (info == null) {
+            // The lookup failed.
+            updatedInfo = null;
+        } else {
+            // If we did not find a matching contact, generate an empty contact info for the number.
+            if (info == ContactInfo.EMPTY) {
+                // Did not find a matching contact.
+                updatedInfo = new ContactInfo();
+                updatedInfo.number = number;
+                updatedInfo.formattedNumber = formatPhoneNumber(number, null, countryIso);
+                updatedInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(
+                        number, countryIso);
+                updatedInfo.lookupUri = createTemporaryContactUri(updatedInfo.formattedNumber);
+            } else {
+                updatedInfo = info;
+            }
+        }
+        return updatedInfo;
+    }
+
+    /**
+     * Creates a JSON-encoded lookup uri for a unknown number without an associated contact
+     *
+     * @param number - Unknown phone number
+     * @return JSON-encoded URI that can be used to perform a lookup when clicking on the quick
+     *         contact card.
+     */
+    private static Uri createTemporaryContactUri(String number) {
+        try {
+            final JSONObject contactRows = new JSONObject().put(Phone.CONTENT_ITEM_TYPE,
+                    new JSONObject().put(Phone.NUMBER, number).put(Phone.TYPE, Phone.TYPE_CUSTOM));
+
+            final String jsonString = new JSONObject().put(Contacts.DISPLAY_NAME, number)
+                    .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.PHONE)
+                    .put(Contacts.CONTENT_ITEM_TYPE, contactRows).toString();
+
+            return Contacts.CONTENT_LOOKUP_URI
+                    .buildUpon()
+                    .appendPath(Constants.LOOKUP_URI_ENCODED)
+                    .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                            String.valueOf(Long.MAX_VALUE))
+                    .encodedFragment(jsonString)
+                    .build();
+        } catch (JSONException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Looks up a contact using the given URI.
+     * <p>
+     * It returns null if an error occurs, {@link ContactInfo#EMPTY} if no matching contact is
+     * found, or the {@link ContactInfo} for the given contact.
+     * <p>
+     * The {@link ContactInfo#formattedNumber} field is always set to {@code null} in the returned
+     * value.
+     */
+    public ContactInfo lookupContactFromUri(Uri uri) {
+        if (uri == null) {
+            return null;
+        }
+        if (!PermissionsUtil.hasContactsPermissions(mContext)) {
+            return ContactInfo.EMPTY;
+        }
+
+        Cursor phoneLookupCursor = null;
+        try {
+            phoneLookupCursor = mContext.getContentResolver().query(uri,
+                    PhoneQuery.PHONE_LOOKUP_PROJECTION, null, null, null);
+        } catch (NullPointerException e) {
+            // Trap NPE from pre-N CP2
+            return null;
+        }
+        if (phoneLookupCursor == null) {
+            return null;
+        }
+
+        try {
+            if (!phoneLookupCursor.moveToFirst()) {
+                return ContactInfo.EMPTY;
+            }
+            String lookupKey = phoneLookupCursor.getString(PhoneQuery.LOOKUP_KEY);
+            ContactInfo contactInfo = createPhoneLookupContactInfo(phoneLookupCursor, lookupKey);
+            contactInfo.nameAlternative = lookUpDisplayNameAlternative(mContext, lookupKey);
+            return contactInfo;
+        } finally {
+            phoneLookupCursor.close();
+        }
+    }
+
+    private ContactInfo createPhoneLookupContactInfo(Cursor phoneLookupCursor, String lookupKey) {
+        ContactInfo info = new ContactInfo();
+        info.lookupKey = lookupKey;
+        info.lookupUri = Contacts.getLookupUri(phoneLookupCursor.getLong(PhoneQuery.PERSON_ID),
+                lookupKey);
+        info.name = phoneLookupCursor.getString(PhoneQuery.NAME);
+        info.type = phoneLookupCursor.getInt(PhoneQuery.PHONE_TYPE);
+        info.label = phoneLookupCursor.getString(PhoneQuery.LABEL);
+        info.number = phoneLookupCursor.getString(PhoneQuery.MATCHED_NUMBER);
+        info.normalizedNumber = phoneLookupCursor.getString(PhoneQuery.NORMALIZED_NUMBER);
+        info.photoId = phoneLookupCursor.getLong(PhoneQuery.PHOTO_ID);
+        info.photoUri = UriUtils.parseUriOrNull(phoneLookupCursor.getString(PhoneQuery.PHOTO_URI));
+        info.formattedNumber = null;
+        // TODO: pass in directory ID rather than null, and make sure it works with work profiles.
+        info.userType = ContactsUtils.determineUserType(null,
+                phoneLookupCursor.getLong(PhoneQuery.PERSON_ID));
+        return info;
+    }
+
+    public static String lookUpDisplayNameAlternative(Context context, String lookupKey) {
+        if (lookupKey == null) {
+            return null;
+        }
+
+        final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
+
+        Cursor cursor = null;
+        try {
+            cursor = context.getContentResolver().query(uri,
+                    NameAlternativeQuery.DISPLAY_NAME_PROJECTION, null, null, null);
+
+            if (cursor != null && cursor.moveToFirst()) {
+                return cursor.getString(NameAlternativeQuery.NAME);
+            }
+        } catch (IllegalArgumentException e) {
+            // Thrown for work profile queries. For those, we don't support
+            // alternative display names.
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Determines the contact information for the given phone number.
+     * <p>
+     * It returns the contact info if found.
+     * <p>
+     * If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
+     * <p>
+     * If the lookup fails for some other reason, it returns null.
+     */
+    private ContactInfo queryContactInfoForPhoneNumber(String number, String countryIso) {
+        if (TextUtils.isEmpty(number)) {
+            return null;
+        }
+
+        ContactInfo info = lookupContactFromUri(getContactInfoLookupUri(number));
+        if (info != null && info != ContactInfo.EMPTY) {
+            info.formattedNumber = formatPhoneNumber(number, null, countryIso);
+        }
+        return info;
+    }
+
+    /**
+     * Format the given phone number
+     *
+     * @param number the number to be formatted.
+     * @param normalizedNumber the normalized number of the given number.
+     * @param countryIso the ISO 3166-1 two letters country code, the country's convention will be
+     *        used to format the number if the normalized phone is null.
+     *
+     * @return the formatted number, or the given number if it was formatted.
+     */
+    private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) {
+        if (TextUtils.isEmpty(number)) {
+            return "";
+        }
+        // If "number" is really a SIP address, don't try to do any formatting at all.
+        if (com.android.contacts.common.util.PhoneNumberHelper.isUriNumber(number)) {
+            return number;
+        }
+        if (TextUtils.isEmpty(countryIso)) {
+            countryIso = mCurrentCountryIso;
+        }
+        return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
+    }
+
+
+    public static Uri getContactInfoLookupUri(String number) {
+        return getContactInfoLookupUri(number, -1);
+    }
+
+    public static Uri getContactInfoLookupUri(String number, long directoryId) {
+        // Get URI for the number in the PhoneLookup table, with a parameter to indicate whether
+        // the number is a SIP number.
+        Uri uri = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
+        if (!ContactsUtils.FLAG_N_FEATURE) {
+            if (directoryId != -1) {
+                // ENTERPRISE_CONTENT_FILTER_URI in M doesn't support directory lookup
+                uri = PhoneLookup.CONTENT_FILTER_URI;
+            } else {
+                // b/25900607 in M. PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, encodes twice.
+                number = Uri.encode(number);
+            }
+        }
+        Uri.Builder builder = uri.buildUpon()
+                .appendPath(number)
+                .appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
+                        String.valueOf(PhoneNumberHelper.isUriNumber(number)));
+        if (directoryId != -1) {
+            builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                    String.valueOf(directoryId));
+        }
+        return builder.build();
+    }
+}
diff --git a/src/com/android/contacts/callblocking/ContentChangedFilter.java b/src/com/android/contacts/callblocking/ContentChangedFilter.java
new file mode 100644
index 0000000..2172c5e
--- /dev/null
+++ b/src/com/android/contacts/callblocking/ContentChangedFilter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+/**
+ * AccessibilityDelegate that will filter out TYPE_WINDOW_CONTENT_CHANGED
+ * Used to suppress "Showing items x of y" from firing of ListView whenever it's content changes.
+ * AccessibilityEvent can only be rejected at a view's parent once it is generated,
+ * use addToParent() to add this delegate to the parent.
+ */
+public class ContentChangedFilter extends AccessibilityDelegate {
+    //the view we don't want TYPE_WINDOW_CONTENT_CHANGED to fire.
+    private View mView;
+
+    /**
+     * Add this delegate to the parent of @param view to filter out TYPE_WINDOW_CONTENT_CHANGED
+     */
+    public static void addToParent(View view){
+        View parent = (View) view.getParent();
+        parent.setAccessibilityDelegate(new ContentChangedFilter(view));
+    }
+
+    private ContentChangedFilter(View view){
+        super();
+        mView = view;
+    }
+
+    @Override
+    public boolean onRequestSendAccessibilityEvent (ViewGroup host, View child, AccessibilityEvent event){
+        if(child == mView){
+            if(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED){
+                return false;
+            }
+        }
+        return super.onRequestSendAccessibilityEvent(host,child,event);
+    }
+
+}
diff --git a/src/com/android/contacts/callblocking/FilteredNumberAsyncQueryHandler.java b/src/com/android/contacts/callblocking/FilteredNumberAsyncQueryHandler.java
new file mode 100644
index 0000000..51f6991
--- /dev/null
+++ b/src/com/android/contacts/callblocking/FilteredNumberAsyncQueryHandler.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+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;
+import android.text.TextUtils;
+
+import com.android.contacts.callblocking.FilteredNumberContract.FilteredNumber;
+import com.android.contacts.callblocking.FilteredNumberContract.FilteredNumberColumns;
+import com.android.contacts.callblocking.FilteredNumberContract.FilteredNumberSources;
+import com.android.contacts.callblocking.FilteredNumberContract.FilteredNumberTypes;
+
+public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler {
+    private static final int NO_TOKEN = 0;
+
+    public FilteredNumberAsyncQueryHandler(ContentResolver cr) {
+        super(cr);
+    }
+
+    /**
+     * Methods for FilteredNumberAsyncQueryHandler result returns.
+     */
+    private static abstract class Listener {
+        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        }
+        protected void onInsertComplete(int token, Object cookie, Uri uri) {
+        }
+        protected void onUpdateComplete(int token, Object cookie, int result) {
+        }
+        protected void onDeleteComplete(int token, Object cookie, int result) {
+        }
+    }
+
+    public interface OnCheckBlockedListener {
+        /**
+         * Invoked after querying if a number is blocked.
+         * @param id The ID of the row if blocked, null otherwise.
+         */
+        void onCheckComplete(Integer id);
+    }
+
+    public interface OnBlockNumberListener {
+        /**
+         * Invoked after inserting a blocked number.
+         * @param uri The uri of the newly created row.
+         */
+        void onBlockComplete(Uri uri);
+    }
+
+    public interface OnUnblockNumberListener {
+        /**
+         * Invoked after removing a blocked number
+         * @param rows The number of rows affected (expected value 1).
+         * @param values The deleted data (used for restoration).
+         */
+        void onUnblockComplete(int rows, ContentValues values);
+    }
+
+    public interface OnHasBlockedNumbersListener {
+        /**
+         * @param hasBlockedNumbers {@code true} if any blocked numbers are stored.
+         *     {@code false} otherwise.
+         */
+        void onHasBlockedNumbers(boolean hasBlockedNumbers);
+    }
+
+    @Override
+    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        if (cookie != null) {
+            ((Listener) cookie).onQueryComplete(token, cookie, cursor);
+        }
+    }
+
+    @Override
+    protected void onInsertComplete(int token, Object cookie, Uri uri) {
+        if (cookie != null) {
+            ((Listener) cookie).onInsertComplete(token, cookie, uri);
+        }
+    }
+
+    @Override
+    protected void onUpdateComplete(int token, Object cookie, int result) {
+        if (cookie != null) {
+            ((Listener) cookie).onUpdateComplete(token, cookie, result);
+        }
+    }
+
+    @Override
+    protected void onDeleteComplete(int token, Object cookie, int result) {
+        if (cookie != null) {
+            ((Listener) cookie).onDeleteComplete(token, cookie, result);
+        }
+    }
+
+    public final void incrementFilteredCount(Integer id) {
+        startUpdate(NO_TOKEN, null,
+                ContentUris.withAppendedId(FilteredNumber.CONTENT_URI_INCREMENT_FILTERED_COUNT, id),
+                null, null, null);
+    }
+
+    public final void hasBlockedNumbers(final OnHasBlockedNumbersListener listener) {
+        startQuery(NO_TOKEN,
+                new Listener() {
+                    @Override
+                    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+                        listener.onHasBlockedNumbers(cursor != null && cursor.getCount() > 0);
+                    }
+                },
+                FilteredNumber.CONTENT_URI,
+                new String[]{ FilteredNumberColumns._ID },
+                FilteredNumberColumns.TYPE + "=" + FilteredNumberTypes.BLOCKED_NUMBER,
+                null,
+                null);
+    }
+
+    /**
+     * Check if this number has been blocked.
+     *
+     * @return {@code false} if the number was invalid and couldn't be checked,
+     *     {@code true} otherwise,
+     */
+    public final boolean isBlockedNumber(
+            final OnCheckBlockedListener listener, String number, String countryIso) {
+        final String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+        if (TextUtils.isEmpty(normalizedNumber)) {
+            return false;
+        }
+
+        startQuery(NO_TOKEN,
+                new Listener() {
+                    @Override
+                    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+                        if (cursor == null || cursor.getCount() != 1) {
+                            listener.onCheckComplete(null);
+                            return;
+                        }
+                        cursor.moveToFirst();
+                        if (cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns.TYPE))
+                                != FilteredNumberTypes.BLOCKED_NUMBER) {
+                            listener.onCheckComplete(null);
+                            return;
+                        }
+                        listener.onCheckComplete(
+                                cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)));
+                    }
+                },
+                FilteredNumber.CONTENT_URI,
+                new String[]{ FilteredNumberColumns._ID, FilteredNumberColumns.TYPE },
+                FilteredNumberColumns.NORMALIZED_NUMBER + " = ?",
+                new String[]{ normalizedNumber },
+                null);
+
+        return true;
+    }
+
+    public final void blockNumber(
+            final OnBlockNumberListener listener, String number, String countryIso) {
+        blockNumber(listener, null, number, countryIso);
+    }
+
+    /**
+     * Add a number manually blocked by the user.
+     */
+    public final void blockNumber(
+            final OnBlockNumberListener listener,
+            String normalizedNumber,
+            String number,
+            String countryIso) {
+        if (normalizedNumber == null) {
+            normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+        }
+        ContentValues v = new ContentValues();
+        v.put(FilteredNumberColumns.NORMALIZED_NUMBER, normalizedNumber);
+        v.put(FilteredNumberColumns.NUMBER, number);
+        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) {
+                        if (listener != null ) {
+                            listener.onBlockComplete(uri);
+                        }
+                    }
+                }, FilteredNumber.CONTENT_URI, values);
+    }
+
+    /**
+     * Removes row from database.
+     * 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) {
+            throw new IllegalArgumentException("Null id passed into unblock");
+        }
+        unblock(listener, ContentUris.withAppendedId(FilteredNumber.CONTENT_URI, 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) {
+                int rowsReturned = cursor == null ? 0 : cursor.getCount();
+                if (rowsReturned != 1) {
+                    throw new SQLiteDatabaseCorruptException
+                            ("Returned " + rowsReturned + " 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) {
+                        if (listener != null) {
+                            listener.onUnblockComplete(result, values);
+                        }
+                    }
+                }, uri, null, null);
+            }
+        }, uri, null, null, null, null);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/callblocking/FilteredNumberContract.java b/src/com/android/contacts/callblocking/FilteredNumberContract.java
new file mode 100644
index 0000000..9f66240
--- /dev/null
+++ b/src/com/android/contacts/callblocking/FilteredNumberContract.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+/**
+ * <p>
+ * The contract between the filtered number provider and applications. Contains
+ * definitions for the supported URIs and columns.
+ * </p>
+ */
+public final class FilteredNumberContract {
+
+    /** The authority for the filtered numbers provider
+     *  Contacts should use this authority from GoogleDialer. */
+    public static final String AUTHORITY =
+            "com.google.android.dialer.provider.filterednumberprovider";
+
+    /** A content:// style uri to the authority for the filtered numbers provider */
+    public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+    /** The type of filtering to be applied, e.g. block the number or whitelist the number. */
+    public interface FilteredNumberTypes {
+        int UNDEFINED = 0;
+        /**
+         * Dialer will disconnect the call without sending the caller to voicemail.
+         */
+        int BLOCKED_NUMBER = 1;
+    }
+
+    /** The original source of the filtered number, e.g. the user manually added it. */
+    public interface FilteredNumberSources {
+        int UNDEFINED = 0;
+        /**
+         * The user manually added this number through Dialer (e.g. from the call log or InCallUI).
+         */
+        int USER = 1;
+    }
+
+    public interface FilteredNumberColumns {
+        // TYPE: INTEGER
+        String _ID = "_id";
+        /**
+         * Represents the number to be filtered, normalized to compare phone numbers for equality.
+         *
+         * TYPE: TEXT
+         */
+        String NORMALIZED_NUMBER = "normalized_number";
+        /**
+         * Represents the number to be filtered, for formatting and
+         * used with country iso for contact lookups.
+         *
+         * TYPE: TEXT
+         */
+        String NUMBER = "number";
+        /**
+         * The country code representing the country detected when
+         * the phone number was added to the database.
+         * Most numbers don't have the country code, so a best guess is provided by
+         * the country detector system. The country iso is also needed in order to format
+         * phone numbers correctly.
+         *
+         * TYPE: TEXT
+         */
+        String COUNTRY_ISO = "country_iso";
+        /**
+         * The number of times the number has been filtered by Dialer.
+         * When this number is incremented, LAST_TIME_FILTERED should also be updated to
+         * the current time.
+         *
+         * TYPE: INTEGER
+         */
+        String TIMES_FILTERED = "times_filtered";
+        /**
+         * Set to the current time when the phone number is filtered.
+         * When this is updated, TIMES_FILTERED should also be incremented.
+         *
+         * TYPE: LONG
+         */
+        String LAST_TIME_FILTERED = "last_time_filtered";
+        // TYPE: LONG
+        String CREATION_TIME = "creation_time";
+        /**
+         * Indicates the type of filtering to be applied.
+         *
+         * TYPE: INTEGER
+         * See {@link FilteredNumberTypes}
+         */
+        String TYPE = "type";
+        /**
+         * Integer representing the original source of the filtered number.
+         *
+         * TYPE: INTEGER
+         * See {@link FilteredNumberSources}
+         */
+        String SOURCE = "source";
+    }
+
+    /**
+     * <p>
+     * Constants for the table of filtered numbers.
+     * </p>
+     * <h3>Operations</h3>
+     * <dl>
+     * <dt><b>Insert</b></dt>
+     * <dd>Required fields: NUMBER, NORMALIZED_NUMBER, TYPE, SOURCE.
+     * A default value will be used for the other fields if left null.</dd>
+     * <dt><b>Update</b></dt>
+     * <dt><b>Delete</b></dt>
+     * <dt><b>Query</b></dt>
+     * <dd>{@link #CONTENT_URI} can be used for any query, append an ID to
+     * retrieve a specific filtered number entry.</dd>
+     * </dl>
+     */
+    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.
+         */
+        private FilteredNumber () {}
+
+        /**
+         * The MIME type of {@link #CONTENT_URI} providing a directory of
+         * filtered numbers.
+         */
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/filtered_numbers_table";
+
+        /**
+         * The MIME type of a {@link #CONTENT_URI} single filtered number.
+         */
+        public static final String CONTENT_ITEM_TYPE =
+                "vnd.android.cursor.item/filtered_numbers_table";
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/callblocking/FilteredNumbersUtil.java b/src/com/android/contacts/callblocking/FilteredNumbersUtil.java
new file mode 100644
index 0000000..4f2dcf0
--- /dev/null
+++ b/src/com/android/contacts/callblocking/FilteredNumbersUtil.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.contacts.R;
+import com.android.contacts.common.util.TelephonyManagerUtils;
+
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.Phonenumber;
+import com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder;
+
+import java.util.Locale;
+
+/**
+ * Utility to help with tasks related to filtered numbers.
+ */
+final public class FilteredNumbersUtil {
+
+    private static final String TAG = "FilteredNumbersUtil";
+
+    public interface CheckForSendToVoicemailContactListener {
+        void onComplete(boolean hasSendToVoicemailContact);
+    }
+
+    public interface ImportSendToVoicemailContactsListener {
+        void onImportComplete();
+    }
+
+    private static class ContactsQuery {
+        static final String[] PROJECTION = {
+            Contacts._ID
+        };
+
+        // TODO: as user can set "send to voicemail" for a contact that doesn't have a phone number,
+        // if those are the only contacts that are marked as "send to voicemail", then when you view
+        // numbers it'll be blank. We should also
+        static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1";
+
+        static final int ID_COLUMN_INDEX = 0;
+    }
+
+    public static class PhoneQuery {
+        static final String[] PROJECTION = {
+            Contacts._ID,
+            Phone.NORMALIZED_NUMBER,
+            Phone.NUMBER
+        };
+
+        static final int ID_COLUMN_INDEX = 0;
+        static final int NORMALIZED_NUMBER_COLUMN_INDEX = 1;
+        static final int NUMBER_COLUMN_INDEX = 2;
+
+        static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1";
+    }
+
+    /**
+     * Checks if there exists a contact with {@code Contacts.SEND_TO_VOICEMAIL} set to true.
+     */
+    public static void checkForSendToVoicemailContact(
+            final Context context, final CheckForSendToVoicemailContactListener listener) {
+        final AsyncTask task = new AsyncTask<Object, Void, Boolean>() {
+            @Override
+            public Boolean doInBackground(Object[]  params) {
+                if (context == null) {
+                    return false;
+                }
+
+                final Cursor cursor = context.getContentResolver().query(
+                        Contacts.CONTENT_URI,
+                        ContactsQuery.PROJECTION,
+                        ContactsQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
+                        null,
+                        null);
+
+                boolean hasSendToVoicemailContacts = false;
+                if (cursor != null) {
+                    try {
+                        hasSendToVoicemailContacts = cursor.getCount() > 0;
+                    } finally {
+                        cursor.close();
+                    }
+                }
+
+                return hasSendToVoicemailContacts;
+            }
+
+            @Override
+            public void onPostExecute(Boolean hasSendToVoicemailContact) {
+                if (listener != null) {
+                    listener.onComplete(hasSendToVoicemailContact);
+                }
+            }
+        };
+        task.execute();
+    }
+
+    /**
+     * Blocks all the phone numbers of any contacts marked as SEND_TO_VOICEMAIL, then clears the
+     * SEND_TO_VOICEMAIL flag on those contacts.
+     */
+    public static void importSendToVoicemailContacts(
+            final Context context, final ImportSendToVoicemailContactsListener listener) {
+        final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler =
+                new FilteredNumberAsyncQueryHandler(context.getContentResolver());
+
+        final AsyncTask<Object, Void, Boolean> task = new AsyncTask<Object, Void, Boolean>() {
+            @Override
+            public Boolean doInBackground(Object[] params) {
+                if (context == null) {
+                    return false;
+                }
+
+                // Get the phone number of contacts marked as SEND_TO_VOICEMAIL.
+                final Cursor phoneCursor = context.getContentResolver().query(
+                        Phone.CONTENT_URI,
+                        PhoneQuery.PROJECTION,
+                        PhoneQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
+                        null,
+                        null);
+
+                if (phoneCursor == null) {
+                    return false;
+                }
+
+                try {
+                    while (phoneCursor.moveToNext()) {
+                        final String normalizedNumber = phoneCursor.getString(
+                                PhoneQuery.NORMALIZED_NUMBER_COLUMN_INDEX);
+                        final String number = phoneCursor.getString(
+                                PhoneQuery.NUMBER_COLUMN_INDEX);
+                        if (normalizedNumber != null) {
+                            // Block the phone number of the contact.
+                            mFilteredNumberAsyncQueryHandler.blockNumber(
+                                    null, normalizedNumber, number, null);
+                        }
+                    }
+                } finally {
+                    phoneCursor.close();
+                }
+
+                // Clear SEND_TO_VOICEMAIL on all contacts. The setting has been imported to Dialer.
+                ContentValues newValues = new ContentValues();
+                newValues.put(Contacts.SEND_TO_VOICEMAIL, 0);
+                context.getContentResolver().update(
+                        Contacts.CONTENT_URI,
+                        newValues,
+                        ContactsQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
+                        null);
+
+                return true;
+            }
+
+            @Override
+            public void onPostExecute(Boolean success) {
+                if (success) {
+                    if (listener != null) {
+                        listener.onImportComplete();
+                    }
+                } else if (context != null) {
+                    String toastStr = context.getString(R.string.send_to_voicemail_import_failed);
+                    Toast.makeText(context, toastStr, Toast.LENGTH_SHORT).show();
+                }
+            }
+        };
+        task.execute();
+    }
+
+    public static boolean canBlockNumber(Context context, String number, String countryIso) {
+        final String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+        return !TextUtils.isEmpty(normalizedNumber)
+                && !PhoneNumberUtils.isEmergencyNumber(normalizedNumber);
+    }
+
+    /**
+     * @return a geographical description string for the specified number.
+     * @see com.android.i18n.phonenumbers.PhoneNumberOfflineGeocoder
+     *
+     * Copied from com.android.dialer.util.PhoneNumberUtil.getGeoDescription(mContext, info.number);
+     */
+    public static String getGeoDescription(Context context, String 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 {
+            pn = util.parse(number, countryIso);
+        } catch (NumberParseException e) {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "getGeoDescription: NumberParseException for incoming number '" +
+                        number + "'");
+            }
+        }
+
+        if (pn != null) {
+            String description = geocoder.getDescriptionForNumber(pn, locale);
+            return description;
+        }
+
+        return null;
+    }
+}
diff --git a/src/com/android/contacts/callblocking/NumbersAdapter.java b/src/com/android/contacts/callblocking/NumbersAdapter.java
new file mode 100644
index 0000000..0622390
--- /dev/null
+++ b/src/com/android/contacts/callblocking/NumbersAdapter.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.app.FragmentManager;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+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.R;
+import com.android.contacts.callblocking.ContactInfo;
+import com.android.contacts.callblocking.ContactInfoHelper;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.util.UriUtils;
+
+import java.util.Locale;
+
+public class NumbersAdapter extends SimpleCursorAdapter {
+
+    private Context mContext;
+    private FragmentManager mFragmentManager;
+    private ContactInfoHelper mContactInfoHelper;
+    private BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
+    private ContactPhotoManager mContactPhotoManager;
+
+    public NumbersAdapter(
+            Context context,
+            FragmentManager fragmentManager,
+            ContactInfoHelper contactInfoHelper,
+            ContactPhotoManager contactPhotoManager) {
+        super(context, R.layout.blocked_number_item, null, new String[]{}, new int[]{}, 0);
+        mContext = context;
+        mFragmentManager = fragmentManager;
+        mContactInfoHelper = contactInfoHelper;
+        mContactPhotoManager = contactPhotoManager;
+    }
+
+    public void updateView(View view, String number, String countryIso) {
+        // TODO: add touch feedback on list item.
+        final TextView callerName = (TextView) view.findViewById(R.id.caller_name);
+        final TextView callerNumber = (TextView) view.findViewById(R.id.caller_number);
+        final QuickContactBadge quickContactBadge =
+                (QuickContactBadge) view.findViewById(R.id.quick_contact_photo);
+        quickContactBadge.setOverlay(null);
+        if (CompatUtils.hasPrioritizedMimeType()) {
+            quickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+        }
+
+        ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
+        if (info == null) {
+            info = new ContactInfo();
+            info.number = number;
+        }
+        final CharSequence locationOrType = getNumberTypeOrLocation(info);
+        final String displayNumber = getDisplayNumber(info);
+        final String displayNumberStr = mBidiFormatter.unicodeWrap(displayNumber,
+                TextDirectionHeuristics.LTR);
+
+        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 DefaultImageRequest request = new DefaultImageRequest(displayName, lookupKey,
+                ContactPhotoManager.TYPE_DEFAULT, /* isCircular */ true);
+        badge.assignContactUri(info.lookupUri);
+        badge.setContentDescription(
+                mContext.getResources().getString(R.string.description_contact_details, displayName));
+        mContactPhotoManager.loadDirectoryPhoto(badge, info.photoUri,
+                /* darkTheme */ false, /* isCircular */ true, 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 Phone.getTypeLabel(
+                    mContext.getResources(), info.type, info.label);
+        } else {
+            return FilteredNumbersUtil.getGeoDescription(mContext, info.number);
+        }
+    }
+
+    protected Context getContext() {
+        return mContext;
+    }
+
+    protected FragmentManager getFragmentManager() {
+        return mFragmentManager;
+    }
+}
diff --git a/src/com/android/contacts/callblocking/OnListFragmentScrolledListener.java b/src/com/android/contacts/callblocking/OnListFragmentScrolledListener.java
new file mode 100644
index 0000000..819bcde
--- /dev/null
+++ b/src/com/android/contacts/callblocking/OnListFragmentScrolledListener.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+/*
+ * Interface to provide callback to activity when a child fragment is scrolled
+ */
+public interface OnListFragmentScrolledListener {
+    void onListFragmentScrollStateChange(int scrollState);
+}
diff --git a/src/com/android/contacts/callblocking/SearchAdapter.java b/src/com/android/contacts/callblocking/SearchAdapter.java
new file mode 100644
index 0000000..ca5908f
--- /dev/null
+++ b/src/com/android/contacts/callblocking/SearchAdapter.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.telephony.PhoneNumberUtils;
+import android.text.BidiFormatter;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.contacts.callblocking.FilteredNumberAsyncQueryHandler;
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.contacts.common.list.PhoneNumberListAdapter;
+import com.android.contacts.common.util.PhoneNumberHelper;
+
+import com.android.contacts.R;
+
+public class SearchAdapter extends PhoneNumberListAdapter {
+
+    private String mFormattedQueryString;
+    private String mCountryIso;
+
+    public final static int SHORTCUT_INVALID = -1;
+    public final static int SHORTCUT_DIRECT_CALL = 0;
+    public final static int SHORTCUT_CREATE_NEW_CONTACT = 1;
+    public final static int SHORTCUT_ADD_TO_EXISTING_CONTACT = 2;
+    public final static int SHORTCUT_SEND_SMS_MESSAGE = 3;
+    public final static int SHORTCUT_MAKE_VIDEO_CALL = 4;
+    public final static int SHORTCUT_BLOCK_NUMBER = 5;
+
+    public final static int SHORTCUT_COUNT = 6;
+
+    private final boolean[] mShortcutEnabled = new boolean[SHORTCUT_COUNT];
+
+    private final BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
+    private boolean mVideoCallingEnabled = false;
+
+    protected boolean mIsQuerySipAddress;
+
+    private Resources mResources;
+    private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+
+    public SearchAdapter(Context context) {
+        super(context);
+        // below is from ContactsPhoneNumberListAdapter
+        mCountryIso = GeoUtil.getCurrentCountryIso(context);
+        mVideoCallingEnabled = CallUtil.isVideoEnabled(context);
+        // below is from RegularSearchListAdapter
+        setShortcutEnabled(SHORTCUT_CREATE_NEW_CONTACT, false);
+        setShortcutEnabled(SHORTCUT_ADD_TO_EXISTING_CONTACT, false);
+        // below is from BlockedListSearchAdapter
+        mResources = context.getResources();
+        disableAllShortcuts();
+        setShortcutEnabled(SHORTCUT_BLOCK_NUMBER, true);
+        mFilteredNumberAsyncQueryHandler =
+                new FilteredNumberAsyncQueryHandler(context.getContentResolver());
+    }
+
+    @Override
+    public int getCount() {
+        return super.getCount() + getShortcutCount();
+    }
+
+    /**
+     * @return The number of enabled shortcuts. Ranges from 0 to a maximum of SHORTCUT_COUNT
+     */
+    public int getShortcutCount() {
+        int count = 0;
+        for (int i = 0; i < mShortcutEnabled.length; i++) {
+            if (mShortcutEnabled[i]) count++;
+        }
+        return count;
+    }
+
+    public void disableAllShortcuts() {
+        for (int i = 0; i < mShortcutEnabled.length; i++) {
+            mShortcutEnabled[i] = false;
+        }
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        final int shortcut = getShortcutTypeFromPosition(position);
+        if (shortcut >= 0) {
+            // shortcutPos should always range from 1 to SHORTCUT_COUNT
+            return super.getViewTypeCount() + shortcut;
+        } else {
+            return super.getItemViewType(position);
+        }
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        // Number of item view types in the super implementation + 2 for the 2 new shortcuts
+        return super.getViewTypeCount() + SHORTCUT_COUNT;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (getShortcutTypeFromPosition(position) >= 0) {
+            if (convertView != null) {
+                assignShortcutToView((ContactListItemView) convertView);
+                return convertView;
+            } else {
+                final ContactListItemView v = new ContactListItemView(getContext(), null,
+                        mVideoCallingEnabled);
+                assignShortcutToView(v);
+                return v;
+            }
+        } else {
+            return super.getView(position, convertView, parent);
+        }
+    }
+
+    @Override
+    protected ContactListItemView newView(
+            Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
+        final ContactListItemView view = super.newView(context, partition, cursor, position,
+                parent);
+
+        view.setSupportVideoCallIcon(mVideoCallingEnabled);
+        return view;
+    }
+
+    /**
+     * @param position The position of the item
+     * @return The enabled shortcut type matching the given position if the item is a
+     * shortcut, -1 otherwise
+     */
+    public int getShortcutTypeFromPosition(int position) {
+        int shortcutCount = position - super.getCount();
+        if (shortcutCount >= 0) {
+            // Iterate through the array of shortcuts, looking only for shortcuts where
+            // mShortcutEnabled[i] is true
+            for (int i = 0; shortcutCount >= 0 && i < mShortcutEnabled.length; i++) {
+                if (mShortcutEnabled[i]) {
+                    shortcutCount--;
+                    if (shortcutCount < 0) return i;
+                }
+            }
+            throw new IllegalArgumentException("Invalid position - greater than cursor count "
+                    + " but not a shortcut.");
+        }
+        return SHORTCUT_INVALID;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return getShortcutCount() == 0 && super.isEmpty();
+    }
+
+    @Override
+    public boolean isEnabled(int position) {
+        final int shortcutType = getShortcutTypeFromPosition(position);
+        if (shortcutType >= 0) {
+            return true;
+        } else {
+            return super.isEnabled(position);
+        }
+    }
+
+    private void assignShortcutToView(ContactListItemView v) {
+        v.setDrawableResource(R.drawable.ic_not_interested_googblue_24dp);
+        v.setDisplayName(
+                getContext().getResources().getString(R.string.search_shortcut_block_number));
+        v.setPhotoPosition(super.getPhotoPosition());
+        v.setAdjustSelectionBoundsEnabled(false);
+    }
+
+    /**
+     * @return True if the shortcut state (disabled vs enabled) was changed by this operation
+     */
+    public boolean setShortcutEnabled(int shortcutType, boolean visible) {
+        final boolean changed = mShortcutEnabled[shortcutType] != visible;
+        mShortcutEnabled[shortcutType] = visible;
+        return changed;
+    }
+
+    public String getFormattedQueryString() {
+        if (mIsQuerySipAddress) {
+            // Return unnormalized SIP address
+            return getQueryString();
+        }
+        return mFormattedQueryString;
+    }
+
+    @Override
+    public void setQueryString(String queryString) {
+        // Don't show actions if the query string contains a letter.
+        final boolean showNumberShortcuts = !TextUtils.isEmpty(getFormattedQueryString())
+                && hasDigitsInQueryString();
+        mIsQuerySipAddress = PhoneNumberHelper.isUriNumber(queryString);
+
+        if (isChanged(showNumberShortcuts)) {
+            notifyDataSetChanged();
+        }
+        mFormattedQueryString = PhoneNumberUtils.formatNumber(
+                PhoneNumberUtils.normalizeNumber(queryString), mCountryIso);
+        super.setQueryString(queryString);
+    }
+
+    protected boolean isChanged(boolean showNumberShortcuts) {
+        return setShortcutEnabled(SHORTCUT_BLOCK_NUMBER, showNumberShortcuts || mIsQuerySipAddress);
+    }
+
+    /**
+     * Whether there is at least one digit in the query string.
+     */
+    private boolean hasDigitsInQueryString() {
+        String queryString = getQueryString();
+        int length = queryString.length();
+        for (int i = 0; i < length; i++) {
+            if (Character.isDigit(queryString.charAt(i))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void setViewBlocked(ContactListItemView view, Integer id) {
+        view.setTag(R.id.block_id, id);
+        final int textColor = mResources.getColor(R.color.blocked_number_block_color);
+        view.getDataView().setTextColor(textColor);
+        view.getLabelView().setTextColor(textColor);
+        //TODO: Add icon
+    }
+
+    public void setViewUnblocked(ContactListItemView view) {
+        view.setTag(R.id.block_id, null);
+        final int textColor = mResources.getColor(R.color.blocked_number_secondary_text_color);
+        view.getDataView().setTextColor(textColor);
+        view.getLabelView().setTextColor(textColor);
+        //TODO: Remove icon
+    }
+
+    @Override
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+        super.bindView(itemView, partition, cursor, position);
+
+        final ContactListItemView view = (ContactListItemView) itemView;
+        // Reset view state to unblocked.
+        setViewUnblocked(view);
+
+        final String number = getPhoneNumber(position);
+        final String countryIso = GeoUtil.getCurrentCountryIso(mContext);
+        final FilteredNumberAsyncQueryHandler.OnCheckBlockedListener onCheckListener =
+                new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() {
+                    @Override
+                    public void onCheckComplete(Integer id) {
+                        if (id != null) {
+                            setViewBlocked(view, id);
+                        }
+                    }
+                };
+        mFilteredNumberAsyncQueryHandler.isBlockedNumber(onCheckListener, number, countryIso);
+    }
+}
diff --git a/src/com/android/contacts/callblocking/SearchFragment.java b/src/com/android/contacts/callblocking/SearchFragment.java
new file mode 100644
index 0000000..a175812
--- /dev/null
+++ b/src/com/android/contacts/callblocking/SearchFragment.java
@@ -0,0 +1,545 @@
+package com.android.contacts.callblocking;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Activity;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v13.app.FragmentCompat;
+import android.telephony.PhoneNumberUtils;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.Space;
+import android.widget.Toast;
+
+import com.android.contacts.R;
+import com.android.contacts.callblocking.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.list.ContactEntryListAdapter;
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.contacts.common.list.PhoneNumberPickerFragment;
+import com.android.contacts.common.list.PinnedHeaderListView;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.contacts.common.util.PermissionsUtil;
+import com.android.contacts.common.util.ViewUtil;
+import com.android.contacts.widget.EmptyContentView;
+import com.android.contacts.widget.SearchEditTextLayout;
+import com.android.contacts.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
+
+import static android.Manifest.permission.READ_CONTACTS;
+
+public class SearchFragment extends PhoneNumberPickerFragment
+        implements OnEmptyViewActionButtonClickedListener,
+        FragmentCompat.OnRequestPermissionsResultCallback,
+        BlockNumberDialogFragment.Callback{
+    private static final String TAG  = SearchFragment.class.getSimpleName();
+
+    public static final int PERMISSION_REQUEST_CODE = 1;
+    private static final int SEARCH_DIRECTORY_RESULT_LIMIT = 5;
+    // copied from packages/apps/InCallUI/src/com/android/incallui/Call.java
+    public static final int INITIATION_REMOTE_DIRECTORY = 4;
+    public static final int INITIATION_REGULAR_SEARCH = 6;
+
+    private OnListFragmentScrolledListener mActivityScrollListener;
+    private View.OnTouchListener mActivityOnTouchListener;
+    private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+    private EditText mSearchView;
+
+    private final TextWatcher mPhoneSearchQueryTextListener = new TextWatcher() {
+        @Override
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+        @Override
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+            setQueryString(s.toString(), false);
+        }
+
+        @Override
+        public void afterTextChanged(Editable s) {}
+    };
+
+    private final SearchEditTextLayout.Callback mSearchLayoutCallback =
+            new SearchEditTextLayout.Callback() {
+                @Override
+                public void onBackButtonClicked() {
+                    getActivity().onBackPressed();
+                }
+
+                @Override
+                public void onSearchViewClicked() {
+                }
+            };
+    /**
+     * Stores the untouched user-entered string that is used to populate the add to contacts
+     * intent.
+     */
+    private String mAddToContactNumber;
+    private int mActionBarHeight;
+    private int mShadowHeight;
+    private int mPaddingTop;
+
+    /**
+     * Used to resize the list view containing search results so that it fits the available space
+     * above the dialpad. Does not have a user-visible effect in regular touch usage (since the
+     * dialpad hides that portion of the ListView anyway), but improves usability in accessibility
+     * mode.
+     */
+    private Space mSpacer;
+
+    private HostInterface mActivity;
+
+    protected EmptyContentView mEmptyView;
+
+    public interface HostInterface {
+        boolean isActionBarShowing();
+        boolean isDialpadShown();
+        int getDialpadHeight();
+        int getActionBarHideOffset();
+        int getActionBarHeight();
+    }
+
+    protected String mPermissionToRequest;
+
+    public SearchFragment() {
+        configureDirectorySearch();
+    }
+
+    public void configureDirectorySearch() {
+        setDirectorySearchEnabled(true);
+        setDirectoryResultLimit(SEARCH_DIRECTORY_RESULT_LIMIT);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setShowEmptyListForNullQuery(true);
+        /*
+         * Pass in the empty string here so ContactEntryListFragment#setQueryString interprets it as
+         * an empty search query, rather than as an uninitalized value. In the latter case, the
+         * adapter returned by #createListAdapter is used, which populates the view with contacts.
+         * Passing in the empty string forces ContactEntryListFragment to interpret it as an empty
+         * query, which results in showing an empty view
+         */
+        setQueryString(getQueryString() == null ? "" : getQueryString(), false);
+        mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(
+                getContext().getContentResolver());
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+        actionBar.setCustomView(R.layout.search_edittext);
+        actionBar.setDisplayShowCustomEnabled(true);
+        actionBar.setDisplayHomeAsUpEnabled(false);
+        actionBar.setDisplayShowHomeEnabled(false);
+
+        final SearchEditTextLayout searchEditTextLayout = (SearchEditTextLayout) actionBar
+                .getCustomView().findViewById(R.id.search_view_container);
+        searchEditTextLayout.expand(true);
+        searchEditTextLayout.setCallback(mSearchLayoutCallback);
+        searchEditTextLayout.setBackgroundDrawable(null);
+
+        mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view);
+        mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener);
+        mSearchView.setHint(R.string.block_number_search_hint);
+
+        searchEditTextLayout.findViewById(R.id.search_box_expanded)
+                .setBackgroundColor(getContext().getResources().getColor(android.R.color.white));
+
+        if (!TextUtils.isEmpty(getQueryString())) {
+            mSearchView.setText(getQueryString());
+        }
+
+        // TODO: Don't set custom text size; use default search text size.
+        mSearchView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+                getResources().getDimension(R.dimen.blocked_number_search_text_size));
+    }
+
+    @Override
+    protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+        super.onCreateView(inflater, container);
+        ((PinnedHeaderListView) getListView()).setScrollToSectionOnHeaderTouch(true);
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        setQuickContactEnabled(true);
+        setAdjustSelectionBoundsEnabled(false);
+        setDarkTheme(false);
+        setPhotoPosition(ContactListItemView.getDefaultPhotoPosition(false /* opposite */));
+        setUseCallableUri(true);
+
+        try {
+            mActivityScrollListener = (OnListFragmentScrolledListener) activity;
+        } catch (ClassCastException e) {
+            Log.d(TAG, activity.toString() + " doesn't implement OnListFragmentScrolledListener. " +
+                    "Ignoring.");
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        if (isSearchMode()) {
+            getAdapter().setHasHeader(0, false);
+        }
+
+        mActivity = (HostInterface) getActivity();
+
+        final Resources res = getResources();
+        mActionBarHeight = mActivity.getActionBarHeight();
+        mShadowHeight  = res.getDrawable(R.drawable.search_shadow).getIntrinsicHeight();
+        mPaddingTop = res.getDimensionPixelSize(R.dimen.search_list_padding_top);
+
+        final View parentView = getView();
+
+        final ListView listView = getListView();
+
+        if (mEmptyView == null) {
+            mEmptyView = new EmptyContentView(getActivity());
+            ((ViewGroup) getListView().getParent()).addView(mEmptyView);
+            getListView().setEmptyView(mEmptyView);
+            setupEmptyView();
+        }
+
+        listView.setBackgroundColor(res.getColor(R.color.background_contacts_results));
+        listView.setClipToPadding(false);
+        setVisibleScrollbarEnabled(false);
+
+        // Turn off accessibility live region as the list constantly update itself and spam
+        // messages.
+        listView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE);
+        ContentChangedFilter.addToParent(listView);
+
+        listView.setOnScrollListener(new OnScrollListener() {
+            @Override
+            public void onScrollStateChanged(AbsListView view, int scrollState) {
+                if (mActivityScrollListener != null) {
+                    mActivityScrollListener.onListFragmentScrollStateChange(scrollState);
+                }
+            }
+
+            @Override
+            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+                                 int totalItemCount) {
+            }
+        });
+        if (mActivityOnTouchListener != null) {
+            listView.setOnTouchListener(mActivityOnTouchListener);
+        }
+
+        updatePosition();
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        ViewUtil.addBottomPaddingToListViewForFab(getListView(), getResources());
+    }
+
+    @Override
+    public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
+        Animator animator = null;
+        if (nextAnim != 0) {
+            animator = AnimatorInflater.loadAnimator(getActivity(), nextAnim);
+        }
+        if (animator != null) {
+            final View view = getView();
+            final int oldLayerType = view.getLayerType();
+            view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            animator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    view.setLayerType(oldLayerType, null);
+                }
+            });
+        }
+        return animator;
+    }
+
+    @Override
+    protected void setSearchMode(boolean flag) {
+        super.setSearchMode(flag);
+        // This hides the "All contacts with phone numbers" header in the search fragment
+        final ContactEntryListAdapter adapter = getAdapter();
+        if (adapter != null) {
+            adapter.setHasHeader(0, false);
+        }
+    }
+
+    public void setAddToContactNumber(String addToContactNumber) {
+        mAddToContactNumber = addToContactNumber;
+    }
+
+    @Override
+    protected ContactEntryListAdapter createListAdapter() {
+        SearchAdapter adapter = new SearchAdapter(getActivity());
+        adapter.setDisplayPhotos(true);
+        // Don't show SIP addresses.
+        adapter.setUseCallableUri(false);
+        // Keep in sync with the queryString set in #onCreate
+        adapter.setQueryString(getQueryString() == null ? "" : getQueryString());
+        return adapter;
+    }
+
+    protected void setupEmptyView() {
+        if (mEmptyView != null && getActivity() != null) {
+            final int imageResource;
+            final int actionLabelResource;
+            final int descriptionResource;
+            final OnEmptyViewActionButtonClickedListener listener;
+            if (!PermissionsUtil.hasPermission(getActivity(), READ_CONTACTS)) {
+                imageResource = R.drawable.empty_contacts;
+                actionLabelResource = R.string.permission_single_turn_on;
+                descriptionResource = R.string.permission_no_search;
+                listener = this;
+                mPermissionToRequest = READ_CONTACTS;
+            } else {
+                imageResource = EmptyContentView.NO_IMAGE;
+                actionLabelResource = EmptyContentView.NO_LABEL;
+                descriptionResource = EmptyContentView.NO_LABEL;
+                listener = null;
+                mPermissionToRequest = null;
+            }
+
+            mEmptyView.setImage(imageResource);
+            mEmptyView.setActionLabel(actionLabelResource);
+            mEmptyView.setDescription(descriptionResource);
+            if (listener != null) {
+                mEmptyView.setActionClickedListener(listener);
+            }
+        }
+    }
+
+    @Override
+    public void onEmptyViewActionButtonClicked() {
+        final Activity activity = getActivity();
+        if (activity == null) {
+            return;
+        }
+
+        if (READ_CONTACTS.equals(mPermissionToRequest)) {
+            FragmentCompat.requestPermissions(this, new String[]{mPermissionToRequest},
+                    PERMISSION_REQUEST_CODE);
+        }
+    }
+
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        super.onItemClick(parent, view, position, id);
+        final int adapterPosition = position - getListView().getHeaderViewsCount();
+        final SearchAdapter adapter = (SearchAdapter) getAdapter();
+        final int shortcutType = adapter.getShortcutTypeFromPosition(adapterPosition);
+        final Integer blockId = (Integer) view.getTag(R.id.block_id);
+        final String number;
+        switch (shortcutType) {
+            case SearchAdapter.SHORTCUT_INVALID:
+                // Handles click on a search result, either contact or nearby places result.
+                number = adapter.getPhoneNumber(adapterPosition);
+                blockContactNumber(number, blockId);
+                break;
+            case SearchAdapter.SHORTCUT_BLOCK_NUMBER:
+                // Handles click on 'Block number' shortcut to add the user query as a number.
+                number = adapter.getQueryString();
+                blockNumber(number);
+                break;
+            default:
+                Log.w(TAG, "Ignoring unsupported shortcut type: " + shortcutType);
+                break;
+        }
+    }
+
+    private void blockNumber(final String number) {
+        final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
+        final OnCheckBlockedListener onCheckListener = new OnCheckBlockedListener() {
+            @Override
+            public void onCheckComplete(Integer id) {
+                if (id == null) {
+                    BlockNumberDialogFragment.show(
+                            id,
+                            number,
+                            countryIso,
+                            PhoneNumberUtils.formatNumber(number, countryIso),
+                            R.id.blocked_numbers_activity_container,
+                            getFragmentManager(),
+                            SearchFragment.this);
+                } else {
+                    Toast.makeText(getContext(),
+                            ContactDisplayUtils.getTtsSpannedPhoneNumber(getResources(),
+                                    R.string.alreadyBlocked, number),
+                            Toast.LENGTH_SHORT).show();
+                }
+            }
+        };
+        final boolean success = mFilteredNumberAsyncQueryHandler.isBlockedNumber(
+                onCheckListener, number, countryIso);
+        if (!success) {
+            Toast.makeText(getContext(),
+                    ContactDisplayUtils.getTtsSpannedPhoneNumber(
+                            getResources(), R.string.invalidNumber, number),
+                    Toast.LENGTH_SHORT).show();
+        }
+    }
+
+
+    @Override
+    protected void onItemClick(int position, long id) {
+        // Prevent super.onItemClicked(int position, long id) from being called.
+    }
+
+    /**
+     * Updates the position and padding of the search fragment.
+     */
+    public void updatePosition() {
+        int endTranslationValue = 0;
+        // Prevents ListView from being translated down after a rotation when the ActionBar is up.
+        if (mActivity.isActionBarShowing()) {
+            endTranslationValue =
+                    mActivity.isDialpadShown() ? 0 : mActionBarHeight - mShadowHeight;
+        }
+        getView().setTranslationY(endTranslationValue);
+        resizeListView();
+
+        // There is padding which should only be applied when the dialpad is not shown.
+        int paddingTop = mActivity.isDialpadShown() ? 0 : mPaddingTop;
+        final ListView listView = getListView();
+        listView.setPaddingRelative(
+                listView.getPaddingStart(),
+                paddingTop,
+                listView.getPaddingEnd(),
+                listView.getPaddingBottom());
+    }
+
+    public void resizeListView() {
+        if (mSpacer == null) {
+            return;
+        }
+        int spacerHeight = mActivity.isDialpadShown() ? mActivity.getDialpadHeight() : 0;
+        if (spacerHeight != mSpacer.getHeight()) {
+            final LinearLayout.LayoutParams lp =
+                    (LinearLayout.LayoutParams) mSpacer.getLayoutParams();
+            lp.height = spacerHeight;
+            mSpacer.setLayoutParams(lp);
+        }
+    }
+
+    @Override
+    protected void startLoading() {
+        if (getActivity() == null) {
+            return;
+        }
+
+        if (PermissionsUtil.hasContactsPermissions(getActivity())) {
+            super.startLoading();
+        } else if (TextUtils.isEmpty(getQueryString())) {
+            // Clear out any existing call shortcuts.
+            final SearchAdapter adapter = (SearchAdapter) getAdapter();
+            adapter.disableAllShortcuts();
+        } else {
+            // The contact list is not going to change (we have no results since permissions are
+            // denied), but the shortcuts might because of the different query, so update the
+            // list.
+            getAdapter().notifyDataSetChanged();
+        }
+
+        setupEmptyView();
+    }
+
+    public void setOnTouchListener(View.OnTouchListener onTouchListener) {
+        mActivityOnTouchListener = onTouchListener;
+    }
+
+    @Override
+    protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+        final LinearLayout parent = (LinearLayout) super.inflateView(inflater, container);
+        final int orientation = getResources().getConfiguration().orientation;
+        if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+            mSpacer = new Space(getActivity());
+            parent.addView(mSpacer,
+                    new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0));
+        }
+        return parent;
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions,
+                                           int[] grantResults) {
+        if (requestCode == PERMISSION_REQUEST_CODE) {
+            setupEmptyView();
+            if (grantResults != null && grantResults.length == 1
+                    && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
+                PermissionsUtil.notifyPermissionGranted(getActivity(), mPermissionToRequest);
+            }
+        }
+    }
+
+    @Override
+    protected int getCallInitiationType(boolean isRemoteDirectory) {
+        return isRemoteDirectory ? INITIATION_REMOTE_DIRECTORY : INITIATION_REGULAR_SEARCH;
+    }
+
+    @Override
+    public void onFilterNumberSuccess() {
+        goBack();
+    }
+
+    @Override
+    public void onUnfilterNumberSuccess() {
+        Log.wtf(TAG, "Unblocked a number from the SearchFragment");
+        goBack();
+    }
+
+    private void goBack() {
+        Activity activity = getActivity();
+        if (activity == null) {
+            return;
+        }
+        activity.onBackPressed();
+    }
+
+    @Override
+    public void onChangeFilteredNumberUndo() {
+        getAdapter().notifyDataSetChanged();
+    }
+
+    private void blockContactNumber(final String number, final Integer blockId) {
+        if (blockId != null) {
+            Toast.makeText(getContext(), ContactDisplayUtils.getTtsSpannedPhoneNumber(
+                            getResources(), R.string.alreadyBlocked, number),
+                    Toast.LENGTH_SHORT).show();
+            return;
+        }
+
+        com.android.contacts.callblocking.BlockNumberDialogFragment.show(
+                blockId,
+                number,
+                com.android.contacts.common.GeoUtil.getCurrentCountryIso(getContext()),
+                number,
+                R.id.blocked_numbers_activity_container,
+                getFragmentManager(),
+                this);
+    }
+}
diff --git a/src/com/android/contacts/callblocking/ViewNumbersToImportAdapter.java b/src/com/android/contacts/callblocking/ViewNumbersToImportAdapter.java
new file mode 100644
index 0000000..c5a14a6
--- /dev/null
+++ b/src/com/android/contacts/callblocking/ViewNumbersToImportAdapter.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.app.FragmentManager;
+import android.database.Cursor;
+import android.content.Context;
+import android.view.View;
+
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.R;
+import com.android.contacts.callblocking.ContactInfoHelper;
+import com.android.contacts.callblocking.FilteredNumbersUtil;
+import com.android.contacts.callblocking.NumbersAdapter;
+
+public class ViewNumbersToImportAdapter extends NumbersAdapter {
+
+    private ViewNumbersToImportAdapter(
+            Context context,
+            FragmentManager fragmentManager,
+            ContactInfoHelper contactInfoHelper,
+            ContactPhotoManager contactPhotoManager) {
+        super(context, fragmentManager, contactInfoHelper, contactPhotoManager);
+    }
+
+    public static ViewNumbersToImportAdapter newViewNumbersToImportAdapter(
+            Context context, FragmentManager fragmentManager) {
+        return new ViewNumbersToImportAdapter(
+                context,
+                fragmentManager,
+                new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)),
+                ContactPhotoManager.getInstance(context));
+    }
+
+    @Override
+    public void bindView(View view, Context context, Cursor cursor) {
+        super.bindView(view, context, cursor);
+
+        final String number = cursor.getString(
+                FilteredNumbersUtil.PhoneQuery.NUMBER_COLUMN_INDEX);
+
+        view.findViewById(R.id.delete_button).setVisibility(View.GONE);
+        updateView(view, number, /* countryIso */ null);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/callblocking/ViewNumbersToImportFragment.java b/src/com/android/contacts/callblocking/ViewNumbersToImportFragment.java
new file mode 100644
index 0000000..d75ade5
--- /dev/null
+++ b/src/com/android/contacts/callblocking/ViewNumbersToImportFragment.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 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.contacts.callblocking;
+
+import android.app.ListFragment;
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.contacts.R;
+import com.android.contacts.callblocking.FilteredNumberContract;
+import com.android.contacts.callblocking.FilteredNumbersUtil;
+import com.android.contacts.callblocking.FilteredNumbersUtil.ImportSendToVoicemailContactsListener;
+
+
+public class ViewNumbersToImportFragment extends ListFragment
+        implements LoaderManager.LoaderCallbacks<Cursor>,
+        View.OnClickListener {
+
+    private ViewNumbersToImportAdapter mAdapter;
+
+    @Override
+    public Context getContext() {
+        return getActivity();
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        if (mAdapter == null) {
+            mAdapter = ViewNumbersToImportAdapter.newViewNumbersToImportAdapter(
+                    getContext(), getActivity().getFragmentManager());
+        }
+        setListAdapter(mAdapter);
+    }
+
+    @Override
+    public void onDestroy() {
+        setListAdapter(null);
+        super.onDestroy();
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        getLoaderManager().initLoader(0, null, this);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+        actionBar.setTitle(R.string.import_send_to_voicemail_numbers_label);
+        actionBar.setDisplayShowCustomEnabled(false);
+        actionBar.setDisplayHomeAsUpEnabled(true);
+        actionBar.setDisplayShowHomeEnabled(true);
+        actionBar.setDisplayShowTitleEnabled(true);
+
+        getActivity().findViewById(R.id.cancel_button).setOnClickListener(this);
+        getActivity().findViewById(R.id.import_button).setOnClickListener(this);
+    }
+
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.view_numbers_to_import_fragment, container, false);
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        final CursorLoader cursorLoader = new CursorLoader(
+                getContext(),
+                Phone.CONTENT_URI,
+                FilteredNumbersUtil.PhoneQuery.PROJECTION,
+                FilteredNumbersUtil.PhoneQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
+                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 view) {
+        switch (view.getId()) {
+            case R.id.import_button:
+                FilteredNumbersUtil.importSendToVoicemailContacts(getContext(),
+                        new ImportSendToVoicemailContactsListener() {
+                            @Override
+                            public void onImportComplete() {
+                                if (getActivity() != null) {
+                                    getActivity().onBackPressed();
+                                }
+                            }
+                        });
+                break;
+            case R.id.cancel_button:
+                getActivity().onBackPressed();
+                break;
+        }
+    }
+}
diff --git a/src/com/android/contacts/widget/EmptyContentView.java b/src/com/android/contacts/widget/EmptyContentView.java
new file mode 100644
index 0000000..12795e3
--- /dev/null
+++ b/src/com/android/contacts/widget/EmptyContentView.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2016 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.contacts.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.contacts.R;
+
+public class EmptyContentView extends LinearLayout implements View.OnClickListener {
+
+    public static final int NO_LABEL = 0;
+    public static final int NO_IMAGE = 0;
+
+    private ImageView mImageView;
+    private TextView mDescriptionView;
+    private TextView mActionView;
+    private OnEmptyViewActionButtonClickedListener mOnActionButtonClickedListener;
+
+    public interface OnEmptyViewActionButtonClickedListener {
+        public void onEmptyViewActionButtonClicked();
+    }
+
+    public EmptyContentView(Context context) {
+        this(context, null);
+    }
+
+    public EmptyContentView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public EmptyContentView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public EmptyContentView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        setOrientation(LinearLayout.VERTICAL);
+
+        final LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(R.layout.empty_content_view, this);
+        // Don't let touches fall through the empty view.
+        setClickable(true);
+        mImageView = (ImageView) findViewById(R.id.emptyListViewImage);
+        mDescriptionView = (TextView) findViewById(R.id.emptyListViewMessage);
+        mActionView = (TextView) findViewById(R.id.emptyListViewAction);
+        mActionView.setOnClickListener(this);
+    }
+
+    public void setDescription(int resourceId) {
+        if (resourceId == NO_LABEL) {
+            mDescriptionView.setText(null);
+            mDescriptionView.setVisibility(View.GONE);
+        } else {
+            mDescriptionView.setText(resourceId);
+            mDescriptionView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    public void setImage(int resourceId) {
+        if (resourceId == NO_LABEL) {
+            mImageView.setImageDrawable(null);
+            mImageView.setVisibility(View.GONE);
+        } else {
+            mImageView.setImageResource(resourceId);
+            mImageView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    public void setActionLabel(int resourceId) {
+        if (resourceId == NO_LABEL) {
+            mActionView.setText(null);
+            mActionView.setVisibility(View.GONE);
+        } else {
+            mActionView.setText(resourceId);
+            mActionView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    public boolean isShowingContent() {
+        return mImageView.getVisibility() == View.VISIBLE
+                || mDescriptionView.getVisibility() == View.VISIBLE
+                || mActionView.getVisibility() == View.VISIBLE;
+    }
+
+    public void setActionClickedListener(OnEmptyViewActionButtonClickedListener listener) {
+        mOnActionButtonClickedListener = listener;
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mOnActionButtonClickedListener != null) {
+            mOnActionButtonClickedListener.onEmptyViewActionButtonClicked();
+        }
+    }
+}
diff --git a/src/com/android/contacts/widget/SearchEditTextLayout.java b/src/com/android/contacts/widget/SearchEditTextLayout.java
new file mode 100644
index 0000000..6d24fbb
--- /dev/null
+++ b/src/com/android/contacts/widget/SearchEditTextLayout.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2016 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.contacts.widget;
+
+import android.content.Context;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+
+import com.android.contacts.R;
+import com.android.phone.common.animation.AnimUtils;
+
+public class SearchEditTextLayout extends FrameLayout {
+    private static final int ANIMATION_DURATION = 200;
+
+    private OnKeyListener mPreImeKeyListener;
+    private int mTopMargin;
+    private int mBottomMargin;
+    private int mLeftMargin;
+    private int mRightMargin;
+
+    /* Subclass-visible for testing */
+    protected boolean mIsExpanded = false;
+    protected boolean mIsFadedOut = false;
+
+    private View mExpanded;
+    private EditText mSearchView;
+    private View mBackButtonView;
+    private View mClearButtonView;
+
+    private Callback mCallback;
+
+    /**
+     * Listener for the back button next to the search view being pressed
+     */
+    public interface Callback {
+        public void onBackButtonClicked();
+        public void onSearchViewClicked();
+    }
+
+    public SearchEditTextLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void setPreImeKeyListener(OnKeyListener listener) {
+        mPreImeKeyListener = listener;
+    }
+
+    public void setCallback(Callback listener) {
+        mCallback = listener;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
+        mTopMargin = params.topMargin;
+        mBottomMargin = params.bottomMargin;
+        mLeftMargin = params.leftMargin;
+        mRightMargin = params.rightMargin;
+
+        mExpanded = findViewById(R.id.search_box_expanded);
+        mSearchView = (EditText) mExpanded.findViewById(R.id.search_view);
+
+        mBackButtonView = findViewById(R.id.search_back_button);
+        mClearButtonView = findViewById(R.id.search_close_button);
+
+        mSearchView.setOnFocusChangeListener(new OnFocusChangeListener() {
+            @Override
+            public void onFocusChange(View v, boolean hasFocus) {
+                if (hasFocus) {
+                    showInputMethod(v);
+                } else {
+                    hideInputMethod(v);
+                }
+            }
+        });
+
+        mSearchView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mCallback != null) {
+                    mCallback.onSearchViewClicked();
+                }
+            }
+        });
+
+        mSearchView.addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            }
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {
+                mClearButtonView.setVisibility(TextUtils.isEmpty(s) ? View.GONE : View.VISIBLE);
+            }
+
+            @Override
+            public void afterTextChanged(Editable s) {
+            }
+        });
+
+        findViewById(R.id.search_close_button).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mSearchView.setText(null);
+            }
+        });
+
+        findViewById(R.id.search_back_button).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mCallback != null) {
+                    mCallback.onBackButtonClicked();
+                }
+            }
+        });
+
+        super.onFinishInflate();
+    }
+
+    @Override
+    public boolean dispatchKeyEventPreIme(KeyEvent event) {
+        if (mPreImeKeyListener != null) {
+            if (mPreImeKeyListener.onKey(this, event.getKeyCode(), event)) {
+                return true;
+            }
+        }
+        return super.dispatchKeyEventPreIme(event);
+    }
+
+    public void fadeOut() {
+        fadeOut(null);
+    }
+
+    public void fadeOut(AnimUtils.AnimationCallback callback) {
+        AnimUtils.fadeOut(this, ANIMATION_DURATION, callback);
+        mIsFadedOut = true;
+    }
+
+    public void fadeIn() {
+        AnimUtils.fadeIn(this, ANIMATION_DURATION);
+        mIsFadedOut = false;
+    }
+
+    public void setVisible(boolean visible) {
+        if (visible) {
+            setAlpha(1);
+            setVisibility(View.VISIBLE);
+            mIsFadedOut = false;
+        } else {
+            setAlpha(0);
+            setVisibility(View.GONE);
+            mIsFadedOut = true;
+        }
+    }
+
+    public void expand(boolean requestFocus) {
+        updateVisibility(true /* isExpand */);
+        mExpanded.setVisibility(View.VISIBLE);
+        mExpanded.setAlpha(1);
+        setMargins(0f);
+        // Set 9-patch background. This owns the padding, so we need to restore the original values.
+        int paddingTop = this.getPaddingTop();
+        int paddingStart = this.getPaddingStart();
+        int paddingBottom = this.getPaddingBottom();
+        int paddingEnd = this.getPaddingEnd();
+        setBackgroundResource(R.drawable.search_shadow);
+        setElevation(0);
+        setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom);
+
+        if (requestFocus) {
+            mSearchView.requestFocus();
+        }
+        mIsExpanded = true;
+    }
+
+    /**
+     * Updates the visibility of views depending on whether we will show the expanded or collapsed
+     * search view. This helps prevent some jank with the crossfading if we are animating.
+     *
+     * @param isExpand Whether we are about to show the expanded search box.
+     */
+    private void updateVisibility(boolean isExpand) {
+        int expandedViewVisibility = isExpand ? View.VISIBLE : View.GONE;
+
+        mBackButtonView.setVisibility(expandedViewVisibility);
+        if (TextUtils.isEmpty(mSearchView.getText())) {
+            mClearButtonView.setVisibility(View.GONE);
+        } else {
+            mClearButtonView.setVisibility(expandedViewVisibility);
+        }
+    }
+
+    public boolean isExpanded() {
+        return mIsExpanded;
+    }
+
+    /**
+     * Assigns margins to the search box as a fraction of its maximum margin size
+     *
+     * @param fraction How large the margins should be as a fraction of their full size
+     */
+    private void setMargins(float fraction) {
+        MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
+        params.topMargin = (int) (mTopMargin * fraction);
+        params.bottomMargin = (int) (mBottomMargin * fraction);
+        params.leftMargin = (int) (mLeftMargin * fraction);
+        params.rightMargin = (int) (mRightMargin * fraction);
+        requestLayout();
+    }
+
+    private void showInputMethod(View view) {
+        final InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(
+                Context.INPUT_METHOD_SERVICE);
+        if (imm != null) {
+            imm.showSoftInput(view, 0);
+        }
+    }
+
+    private void hideInputMethod(View view) {
+        final InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(
+                Context.INPUT_METHOD_SERVICE);
+        if (imm != null) {
+            imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+        }
+    }
+}