Copy "Blocked Numbers Activity" from Dialer to Contacts
1. Classes are copied and modified so that they will work in Contacts.
2. BlockedNumbersActivity.java is the main activity.
3. Most copied classes are put in the newly-created "callblocking" package.
4. What's not copied: CachedNumberLookupService, visual voicemail, and
emergency call. The corresponding features will be implemented based on
framework change, which is not ready yet.
5. In Dialer, BlockedListSearchFragment extends RegularSearchFragment,
which extends SearchFragment. These three classes are combined into
SearchFragment in Contacts.
6. In Dialer, BlockedListSearchAdapter extends RegularSearchListAdapter,
which extends DialerPhoneNumberListAdapter. These three classes are
combined into SearchAdapter in Contacts.
7. An intent is specified in AndroidManifest.xml to open
BlockedNumbersActivity.java
Bug: 26453530
Change-Id: Iec07725fd9aa5a174bb6b306792fa446dcaa4e65
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);
+ }
+ }
+}