summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChiao Cheng <chiaocheng@google.com>2012-11-28 18:06:44 -0800
committerChiao Cheng <chiaocheng@google.com>2012-12-03 11:50:42 -0800
commitba2c125b07086a88a3517fcf381a3a400c42afd3 (patch)
treef524ecf7e9e00030b060135e668066065375fccf
parent8e3d59a4b2f9140f0b612055cf149cf73e4dde6d (diff)
downloadandroid_packages_apps_ContactsCommon-ba2c125b07086a88a3517fcf381a3a400c42afd3.tar.gz
android_packages_apps_ContactsCommon-ba2c125b07086a88a3517fcf381a3a400c42afd3.tar.bz2
android_packages_apps_ContactsCommon-ba2c125b07086a88a3517fcf381a3a400c42afd3.zip
Further clean-up of PhoneFavoriteFragment in Dialer app.
Moving dependencies of Dialer PhoneFavoriteFragment. Mostly filtering dependencies. Bug: 6993891 Change-Id: Ic2b29b80ae2367f54e619b30bdb71b098c8a0deb
-rw-r--r--res/drawable-hdpi/badge_action_call.pngbin0 -> 1384 bytes
-rw-r--r--res/drawable-hdpi/badge_action_sms.pngbin0 -> 562 bytes
-rw-r--r--res/drawable-hdpi/ic_menu_settings_holo_light.pngbin0 -> 1219 bytes
-rw-r--r--res/drawable-hdpi/ic_menu_star_holo_light.pngbin0 -> 1211 bytes
-rw-r--r--res/drawable-hdpi/unknown_source.pngbin0 -> 4333 bytes
-rw-r--r--res/drawable-mdpi/badge_action_call.pngbin0 -> 932 bytes
-rw-r--r--res/drawable-mdpi/badge_action_sms.pngbin0 -> 504 bytes
-rw-r--r--res/drawable-mdpi/ic_menu_settings_holo_light.pngbin0 -> 850 bytes
-rw-r--r--res/drawable-mdpi/ic_menu_star_holo_light.pngbin0 -> 884 bytes
-rw-r--r--res/drawable-mdpi/unknown_source.pngbin0 -> 2059 bytes
-rw-r--r--res/drawable-xhdpi/badge_action_call.pngbin0 -> 1968 bytes
-rw-r--r--res/drawable-xhdpi/badge_action_sms.pngbin0 -> 711 bytes
-rw-r--r--res/drawable-xhdpi/ic_menu_settings_holo_light.pngbin0 -> 1638 bytes
-rw-r--r--res/drawable-xhdpi/ic_menu_star_holo_light.pngbin0 -> 1607 bytes
-rw-r--r--res/drawable-xhdpi/unknown_source.pngbin0 -> 6486 bytes
-rw-r--r--res/layout/account_filter_header.xml35
-rw-r--r--res/layout/contact_list_content.xml64
-rw-r--r--res/layout/contact_list_filter.xml39
-rw-r--r--res/layout/contact_list_filter_custom.xml62
-rw-r--r--res/layout/contact_list_filter_item.xml66
-rw-r--r--res/layout/custom_contact_list_filter_account.xml56
-rw-r--r--res/layout/custom_contact_list_filter_group.xml71
-rw-r--r--res/layout/event_field_editor_view.xml23
-rw-r--r--res/layout/footer_panel.xml41
-rw-r--r--res/values-sw580dp/dimens.xml3
-rw-r--r--res/values-sw580dp/styles.xml24
-rw-r--r--res/values/attrs.xml8
-rw-r--r--res/values/colors.xml5
-rw-r--r--res/values/dimens.xml25
-rw-r--r--res/values/strings.xml47
-rw-r--r--res/values/styles.xml14
-rw-r--r--src/com/android/contacts/common/list/AccountFilterActivity.java263
-rw-r--r--src/com/android/contacts/common/list/ContactListFilterView.java142
-rw-r--r--src/com/android/contacts/common/list/CustomContactListFilterActivity.java923
-rw-r--r--src/com/android/contacts/common/list/ShortcutIntentBuilder.java421
-rw-r--r--src/com/android/contacts/common/util/AccountFilterUtil.java166
-rw-r--r--src/com/android/contacts/common/util/EmptyService.java33
-rw-r--r--src/com/android/contacts/common/util/LocalizedNameResolver.java161
-rw-r--r--src/com/android/contacts/common/util/WeakAsyncTask.java69
39 files changed, 2749 insertions, 12 deletions
diff --git a/res/drawable-hdpi/badge_action_call.png b/res/drawable-hdpi/badge_action_call.png
new file mode 100644
index 00000000..0b1c6b45
--- /dev/null
+++ b/res/drawable-hdpi/badge_action_call.png
Binary files differ
diff --git a/res/drawable-hdpi/badge_action_sms.png b/res/drawable-hdpi/badge_action_sms.png
new file mode 100644
index 00000000..0dfdbf5a
--- /dev/null
+++ b/res/drawable-hdpi/badge_action_sms.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_settings_holo_light.png b/res/drawable-hdpi/ic_menu_settings_holo_light.png
new file mode 100644
index 00000000..b7bb5c41
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_settings_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_star_holo_light.png b/res/drawable-hdpi/ic_menu_star_holo_light.png
new file mode 100644
index 00000000..45137967
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_star_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/unknown_source.png b/res/drawable-hdpi/unknown_source.png
new file mode 100644
index 00000000..0a8f37d7
--- /dev/null
+++ b/res/drawable-hdpi/unknown_source.png
Binary files differ
diff --git a/res/drawable-mdpi/badge_action_call.png b/res/drawable-mdpi/badge_action_call.png
new file mode 100644
index 00000000..af2abaae
--- /dev/null
+++ b/res/drawable-mdpi/badge_action_call.png
Binary files differ
diff --git a/res/drawable-mdpi/badge_action_sms.png b/res/drawable-mdpi/badge_action_sms.png
new file mode 100644
index 00000000..13dd8bc1
--- /dev/null
+++ b/res/drawable-mdpi/badge_action_sms.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_settings_holo_light.png b/res/drawable-mdpi/ic_menu_settings_holo_light.png
new file mode 100644
index 00000000..1ebc112e
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_settings_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_star_holo_light.png b/res/drawable-mdpi/ic_menu_star_holo_light.png
new file mode 100644
index 00000000..8263b27b
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_star_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/unknown_source.png b/res/drawable-mdpi/unknown_source.png
new file mode 100644
index 00000000..356748f0
--- /dev/null
+++ b/res/drawable-mdpi/unknown_source.png
Binary files differ
diff --git a/res/drawable-xhdpi/badge_action_call.png b/res/drawable-xhdpi/badge_action_call.png
new file mode 100644
index 00000000..2589e334
--- /dev/null
+++ b/res/drawable-xhdpi/badge_action_call.png
Binary files differ
diff --git a/res/drawable-xhdpi/badge_action_sms.png b/res/drawable-xhdpi/badge_action_sms.png
new file mode 100644
index 00000000..460451f9
--- /dev/null
+++ b/res/drawable-xhdpi/badge_action_sms.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_settings_holo_light.png b/res/drawable-xhdpi/ic_menu_settings_holo_light.png
new file mode 100644
index 00000000..68ba92bd
--- /dev/null
+++ b/res/drawable-xhdpi/ic_menu_settings_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_star_holo_light.png b/res/drawable-xhdpi/ic_menu_star_holo_light.png
new file mode 100644
index 00000000..90679117
--- /dev/null
+++ b/res/drawable-xhdpi/ic_menu_star_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/unknown_source.png b/res/drawable-xhdpi/unknown_source.png
new file mode 100644
index 00000000..35e8fb4a
--- /dev/null
+++ b/res/drawable-xhdpi/unknown_source.png
Binary files differ
diff --git a/res/layout/account_filter_header.xml b/res/layout/account_filter_header.xml
new file mode 100644
index 00000000..0ffb7e1c
--- /dev/null
+++ b/res/layout/account_filter_header.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<!-- Layout showing the type of account filter
+ (e.g. All contacts filter, custom filter, etc.),
+ which is the header of all contact lists. -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/account_filter_header_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="@dimen/list_header_extra_top_padding"
+ android:layout_marginLeft="@dimen/contact_browser_list_header_left_margin"
+ android:layout_marginRight="@dimen/contact_browser_list_header_right_margin"
+ android:background="?android:attr/selectableItemBackground"
+ android:visibility="gone">
+ <TextView
+ android:id="@+id/account_filter_header"
+ style="@style/ContactListSeparatorTextViewStyle"
+ android:paddingLeft="@dimen/contact_browser_list_item_text_indent" />
+</LinearLayout>
diff --git a/res/layout/contact_list_content.xml b/res/layout/contact_list_content.xml
new file mode 100644
index 00000000..362209cc
--- /dev/null
+++ b/res/layout/contact_list_content.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<!-- android:paddingTop is used instead of android:layout_marginTop. It looks
+ android:layout_marginTop is ignored when used with <fragment></fragment>, which
+ only happens in Tablet UI since we rely on ViewPager in Phone UI.
+ Instead, android:layout_marginTop inside <fragment /> is effective. -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pinned_header_list_layout"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/contact_browser_background" >
+
+ <!-- Shown only when an Account filter is set.
+ - paddingTop should be here to show "shade" effect correctly. -->
+ <include
+ android:id="@+id/account_filter_header_container"
+ layout="@layout/account_filter_header" />
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1" >
+ <view
+ class="com.android.contacts.common.list.PinnedHeaderListView"
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="?attr/contact_browser_list_padding_left"
+ android:layout_marginRight="?attr/contact_browser_list_padding_right"
+ android:fastScrollEnabled="true"
+ android:fadingEdge="none" />
+ <ProgressBar
+ android:id="@+id/search_progress"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone" />
+ </FrameLayout>
+
+ <ViewStub
+ android:id="@+id/footer_stub"
+ android:layout="@layout/footer_panel"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/res/layout/contact_list_filter.xml b/res/layout/contact_list_filter.xml
new file mode 100644
index 00000000..f47d309f
--- /dev/null
+++ b/res/layout/contact_list_filter.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2011 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:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:fillViewport="true">
+
+ <ListView
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:layout_marginLeft="@dimen/contact_filter_left_margin"
+ android:layout_marginRight="@dimen/contact_filter_right_margin" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:background="?android:attr/dividerHorizontal" />
+</LinearLayout>
diff --git a/res/layout/contact_list_filter_custom.xml b/res/layout/contact_list_filter_custom.xml
new file mode 100644
index 00000000..78318bca
--- /dev/null
+++ b/res/layout/contact_list_filter_custom.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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"
+ style="@style/CustomContactListFilterView"
+ android:orientation="vertical"
+ android:fillViewport="true">
+
+ <ExpandableListView
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:layout_marginLeft="@dimen/contact_filter_left_margin"
+ android:layout_marginRight="@dimen/contact_filter_right_margin"
+ android:overScrollMode="always" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:background="?android:attr/dividerHorizontal" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ style="?android:attr/buttonBarStyle">
+
+ <Button
+ android:id="@+id/btn_discard"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@android:string/cancel" />
+
+ <Button
+ android:id="@+id/btn_done"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@android:string/ok" />
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/contact_list_filter_item.xml b/res/layout/contact_list_filter_item.xml
new file mode 100644
index 00000000..ec6a45ed
--- /dev/null
+++ b/res/layout/contact_list_filter_item.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+
+<view
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ class="com.android.contacts.common.list.ContactListFilterView"
+ android:descendantFocusability="blocksDescendants"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/contact_filter_item_min_height"
+ android:gravity="center_vertical">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:scaleType="fitCenter"
+ android:layout_width="@dimen/contact_filter_icon_size"
+ android:layout_height="@dimen/contact_filter_icon_size"/>
+
+ <LinearLayout
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:layout_marginLeft="8dip">
+
+ <TextView
+ android:id="@+id/accountType"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="end"/>
+
+ <TextView
+ android:id="@+id/accountUserName"
+ android:layout_marginTop="-3dip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorTertiary"
+ android:singleLine="true"
+ android:ellipsize="end"/>
+ </LinearLayout>
+
+ <RadioButton
+ android:id="@+id/radioButton"
+ android:clickable="false"
+ android:layout_marginTop="1dip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical" />
+</view>
+
diff --git a/res/layout/custom_contact_list_filter_account.xml b/res/layout/custom_contact_list_filter_account.xml
new file mode 100644
index 00000000..6b25fed9
--- /dev/null
+++ b/res/layout/custom_contact_list_filter_account.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:gravity="center_vertical"
+ android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft"
+ android:paddingRight="?android:attr/scrollbarSize">
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="6dip"
+ android:layout_marginTop="6dip"
+ android:layout_marginBottom="6dip"
+ android:layout_weight="1"
+ android:duplicateParentState="true">
+
+ <TextView
+ android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:duplicateParentState="true" />
+
+ <TextView
+ android:id="@android:id/text2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/text1"
+ android:layout_alignLeft="@android:id/text1"
+ android:maxLines="1"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorTertiary"
+ android:duplicateParentState="true" />
+
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/res/layout/custom_contact_list_filter_group.xml b/res/layout/custom_contact_list_filter_group.xml
new file mode 100644
index 00000000..a9de606c
--- /dev/null
+++ b/res/layout/custom_contact_list_filter_group.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:gravity="center_vertical"
+ android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft"
+ android:paddingRight="?android:attr/scrollbarSize"
+>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="6dip"
+ android:layout_marginTop="6dip"
+ android:layout_marginBottom="6dip"
+ android:layout_weight="1"
+ android:duplicateParentState="true"
+ >
+
+ <TextView
+ android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:duplicateParentState="true"
+ />
+
+ <TextView
+ android:id="@android:id/text2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/text1"
+ android:layout_alignLeft="@android:id/text1"
+ android:maxLines="2"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:duplicateParentState="true"
+ />
+
+ </RelativeLayout>
+
+ <CheckBox
+ android:id="@android:id/checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="4dip"
+ android:focusable="false"
+ android:clickable="false"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:duplicateParentState="true"
+ />
+
+</LinearLayout>
diff --git a/res/layout/event_field_editor_view.xml b/res/layout/event_field_editor_view.xml
index 560b9e1d..040272c4 100644
--- a/res/layout/event_field_editor_view.xml
+++ b/res/layout/event_field_editor_view.xml
@@ -1,18 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2011 The Android Open Source Project
+<!--
+ Copyright (C) 2012 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
+ 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
+ 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.
--->
+ 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.
+ -->
<!-- Editor for a single event entry in the contact editor -->
diff --git a/res/layout/footer_panel.xml b/res/layout/footer_panel.xml
new file mode 100644
index 00000000..2625a43e
--- /dev/null
+++ b/res/layout/footer_panel.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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/footer"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ style="@android:style/ButtonBar"
+>
+
+ <Button android:id="@+id/done"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/menu_done"
+ />
+
+ <Button android:id="@+id/revert"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/menu_doNotSave"
+ />
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/values-sw580dp/dimens.xml b/res/values-sw580dp/dimens.xml
index 45618244..277c0297 100644
--- a/res/values-sw580dp/dimens.xml
+++ b/res/values-sw580dp/dimens.xml
@@ -21,4 +21,7 @@
<dimen name="editor_round_button_padding_right">16dip</dimen>
<dimen name="editor_type_label_width">122dip</dimen>
+
+ <dimen name="contact_browser_list_header_left_margin">@dimen/list_visible_scrollbar_padding</dimen>
+ <dimen name="contact_browser_list_header_right_margin">24dip</dimen>
</resources>
diff --git a/res/values-sw580dp/styles.xml b/res/values-sw580dp/styles.xml
new file mode 100644
index 00000000..30dbbe7e
--- /dev/null
+++ b/res/values-sw580dp/styles.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+
+ <style name="CustomContactListFilterView" parent="ContactListFilterTheme">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">400dip</item>
+ </style>
+
+</resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index eae9c36f..745ceb19 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -11,7 +11,7 @@
~ 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
+ ~ limitations under the License.
-->
<resources>
@@ -45,4 +45,10 @@
<attr name="list_item_data_width_weight" format="integer"/>
<attr name="list_item_label_width_weight" format="integer"/>
</declare-styleable>
+
+ <declare-styleable name="ContactBrowser">
+ <attr name="contact_browser_list_padding_left" format="dimension"/>
+ <attr name="contact_browser_list_padding_right" format="dimension"/>
+ <attr name="contact_browser_background" format="reference"/>
+ </declare-styleable>
</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 48ef4ece..76000877 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -33,4 +33,9 @@
<!-- Color of the status message for starred contacts in the People app -->
<color name="people_contact_tile_status_color">#CCCCCC</color>
+
+ <color name="shortcut_overlay_text_background">#7f000000</color>
+
+ <color name="textColorIconOverlay">#fff</color>
+ <color name="textColorIconOverlayShadow">#000</color>
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 8901aea1..43c29e87 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -58,4 +58,29 @@
<!-- Width of the Type-Label in the Editor -->
<dimen name="editor_type_label_width">100dip</dimen>
+
+ <!-- For contact filter setting screens -->
+ <dimen name="contact_filter_left_margin">16dip</dimen>
+ <dimen name="contact_filter_right_margin">16dip</dimen>
+ <dimen name="contact_filter_item_min_height">48dip</dimen>
+ <dimen name="contact_filter_icon_size">32dip</dimen>
+
+ <!-- Padding to be used between a visible scrollbar and the contact list -->
+ <dimen name="list_visible_scrollbar_padding">32dip</dimen>
+
+ <dimen name="contact_browser_list_header_left_margin">16dip</dimen>
+ <dimen name="contact_browser_list_header_right_margin">@dimen/list_visible_scrollbar_padding</dimen>
+ <dimen name="contact_browser_list_item_text_indent">8dip</dimen>
+
+ <!-- Size of the shortcut icon. 0dip means: use the system default -->
+ <dimen name="shortcut_icon_size">0dip</dimen>
+
+ <!-- Width of darkened border for shortcut icon -->
+ <dimen name="shortcut_icon_border_width">1dp</dimen>
+
+ <!-- Text size of shortcut icon overlay text -->
+ <dimen name="shortcut_overlay_text_size">12dp</dimen>
+
+ <!-- Extra vertical padding for darkened background behind shortcut icon overlay text -->
+ <dimen name="shortcut_overlay_text_background_padding">1dp</dimen>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index fc966eef..12c906e4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -366,4 +366,51 @@
Middle Name, Last Name, Name Suffix).
[CHAR LIMIT=NONE] -->
<string name="expand_collapse_name_fields_description">Expand or collapse name fields</string>
+
+
+ <!-- Contact list filter label indicating that the list is showing all available accounts [CHAR LIMIT=64] -->
+ <string name="list_filter_all_accounts">All contacts</string>
+
+ <!-- Contact list filter label indicating that the list is showing all starred contacts [CHAR LIMIT=64] -->
+ <string name="list_filter_all_starred">Starred</string>
+
+ <!-- Contact list filter selection indicating that the list shows groups chosen by the user [CHAR LIMIT=64] -->
+ <string name="list_filter_customize">Customize</string>
+
+ <!-- Contact list filter selection indicating that the list shows only the selected contact [CHAR LIMIT=64] -->
+ <string name="list_filter_single">Contact</string>
+
+ <!-- List title for a special contacts group that covers all contacts. [CHAR LIMIT=25] -->
+ <string name="display_ungrouped">All other contacts</string>
+
+ <!-- List title for a special contacts group that covers all contacts that
+a ren't members of any other group. [CHAR LIMIT=25] -->
+ <string name="display_all_contacts">All contacts</string>
+
+ <string name="menu_sync_remove">Remove sync group</string>
+ <string name="dialog_sync_add">Add sync group</string>
+ <string name="display_more_groups">More groups\u2026</string>
+
+ <!-- Warning message given to users just before they remove a currently syncing
+ group that would also cause all ungrouped contacts to stop syncing. [CHAR LIMIT=NONE] -->
+ <string name="display_warn_remove_ungrouped">Removing \"<xliff:g id="group" example="Starred">%s</xliff:g>\" from sync will also remove any ungrouped contacts from sync.</string>
+
+ <!-- Displayed in a spinner dialog as user changes to display options are saved -->
+ <string name="savingDisplayGroups">Saving display options\u2026</string>
+
+ <!-- Menu item to indicate you are done editing a contact and want to save the changes you've made -->
+ <string name="menu_done">Done</string>
+
+ <!-- Menu item to indicate you want to cancel the current editing process and NOT save the changes you've made [CHAR LIMIT=12] -->
+ <string name="menu_doNotSave">Cancel</string>
+
+ <!-- Displayed at the top of the contacts showing the account filter selected [CHAR LIMIT=64] -->
+ <string name="listAllContactsInAccount">Contacts in <xliff:g id="name" example="abc@gmail.com">%s</xliff:g></string>
+
+ <!-- Displayed at the top of the contacts showing single contact. [CHAR LIMIT=64] -->
+ <string name="listCustomView">Contacts in custom view</string>
+
+ <!-- Displayed at the top of the contacts showing single contact. [CHAR LIMIT=64] -->
+ <string name="listSingleContact">Single contact</string>
+
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 7522f99a..7a43243b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -40,4 +40,18 @@ background and text color. See also android:style/Widget.Holo.TextView.ListSepar
<item name="android:singleLine">true</item>
<item name="android:textAllCaps">true</item>
</style>
+
+ <style name="ListViewStyle" parent="@android:style/Widget.Holo.Light.ListView">
+ <item name="android:overScrollMode">always</item>
+ </style>
+
+ <style name="ContactListFilterTheme" parent="@android:Theme.Holo.Light.Dialog">
+ <item name="android:windowCloseOnTouchOutside">true</item>
+ <item name="android:listViewStyle">@style/ListViewStyle</item>
+ </style>
+
+ <style name="CustomContactListFilterView" parent="ContactListFilterTheme">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">match_parent</item>
+ </style>
</resources>
diff --git a/src/com/android/contacts/common/list/AccountFilterActivity.java b/src/com/android/contacts/common/list/AccountFilterActivity.java
new file mode 100644
index 00000000..58450c65
--- /dev/null
+++ b/src/com/android/contacts/common/list/AccountFilterActivity.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2011 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.common.list;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Shows a list of all available accounts, letting the user select under which account to view
+ * contacts.
+ */
+public class AccountFilterActivity extends Activity implements AdapterView.OnItemClickListener {
+
+ private static final String TAG = AccountFilterActivity.class.getSimpleName();
+
+ private static final int SUBACTIVITY_CUSTOMIZE_FILTER = 0;
+
+ public static final String KEY_EXTRA_CONTACT_LIST_FILTER = "contactListFilter";
+ public static final String KEY_EXTRA_CURRENT_FILTER = "currentFilter";
+
+ private static final int FILTER_LOADER_ID = 0;
+
+ private ListView mListView;
+
+ private ContactListFilter mCurrentFilter;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.contact_list_filter);
+
+ mListView = (ListView) findViewById(android.R.id.list);
+ mListView.setOnItemClickListener(this);
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ mCurrentFilter = getIntent().getParcelableExtra(KEY_EXTRA_CURRENT_FILTER);
+
+ getLoaderManager().initLoader(FILTER_LOADER_ID, null, new MyLoaderCallbacks());
+ }
+
+ private static class FilterLoader extends AsyncTaskLoader<List<ContactListFilter>> {
+ private Context mContext;
+
+ public FilterLoader(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ @Override
+ public List<ContactListFilter> loadInBackground() {
+ return loadAccountFilters(mContext);
+ }
+
+ @Override
+ protected void onStartLoading() {
+ forceLoad();
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ protected void onReset() {
+ onStopLoading();
+ }
+ }
+
+ private static List<ContactListFilter> loadAccountFilters(Context context) {
+ final ArrayList<ContactListFilter> result = Lists.newArrayList();
+ final ArrayList<ContactListFilter> accountFilters = Lists.newArrayList();
+ final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context);
+ List<AccountWithDataSet> accounts = accountTypes.getAccounts(false);
+ for (AccountWithDataSet account : accounts) {
+ AccountType accountType = accountTypes.getAccountType(account.type, account.dataSet);
+ if (accountType.isExtension() && !account.hasData(context)) {
+ // Hide extensions with no raw_contacts.
+ continue;
+ }
+ Drawable icon = accountType != null ? accountType.getDisplayIcon(context) : null;
+ accountFilters.add(ContactListFilter.createAccountFilter(
+ account.type, account.name, account.dataSet, icon));
+ }
+
+ // Always show "All", even when there's no accounts. (We may have local contacts)
+ result.add(ContactListFilter.createFilterWithType(
+ ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS));
+
+ final int count = accountFilters.size();
+ if (count >= 1) {
+ // If we only have one account, don't show it as "account", instead show it as "all"
+ if (count > 1) {
+ result.addAll(accountFilters);
+ }
+ result.add(ContactListFilter.createFilterWithType(
+ ContactListFilter.FILTER_TYPE_CUSTOM));
+ }
+ return result;
+ }
+
+ private class MyLoaderCallbacks implements LoaderCallbacks<List<ContactListFilter>> {
+ @Override
+ public Loader<List<ContactListFilter>> onCreateLoader(int id, Bundle args) {
+ return new FilterLoader(AccountFilterActivity.this);
+ }
+
+ @Override
+ public void onLoadFinished(
+ Loader<List<ContactListFilter>> loader, List<ContactListFilter> data) {
+ if (data == null) { // Just in case...
+ Log.e(TAG, "Failed to load filters");
+ return;
+ }
+ mListView.setAdapter(
+ new FilterListAdapter(AccountFilterActivity.this, data, mCurrentFilter));
+ }
+
+ @Override
+ public void onLoaderReset(Loader<List<ContactListFilter>> loader) {
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final ContactListFilter filter = (ContactListFilter) view.getTag();
+ if (filter == null) return; // Just in case
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+ final Intent intent = new Intent(this,
+ CustomContactListFilterActivity.class);
+ startActivityForResult(intent, SUBACTIVITY_CUSTOMIZE_FILTER);
+ } else {
+ final Intent intent = new Intent();
+ intent.putExtra(KEY_EXTRA_CONTACT_LIST_FILTER, filter);
+ setResult(Activity.RESULT_OK, intent);
+ finish();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode != Activity.RESULT_OK) {
+ return;
+ }
+
+ switch (requestCode) {
+ case SUBACTIVITY_CUSTOMIZE_FILTER: {
+ final Intent intent = new Intent();
+ ContactListFilter filter = ContactListFilter.createFilterWithType(
+ ContactListFilter.FILTER_TYPE_CUSTOM);
+ intent.putExtra(KEY_EXTRA_CONTACT_LIST_FILTER, filter);
+ setResult(Activity.RESULT_OK, intent);
+ finish();
+ break;
+ }
+ }
+ }
+
+ private static class FilterListAdapter extends BaseAdapter {
+ private final List<ContactListFilter> mFilters;
+ private final LayoutInflater mLayoutInflater;
+ private final AccountTypeManager mAccountTypes;
+ private final ContactListFilter mCurrentFilter;
+
+ public FilterListAdapter(
+ Context context, List<ContactListFilter> filters, ContactListFilter current) {
+ mLayoutInflater = (LayoutInflater) context.getSystemService
+ (Context.LAYOUT_INFLATER_SERVICE);
+ mFilters = filters;
+ mCurrentFilter = current;
+ mAccountTypes = AccountTypeManager.getInstance(context);
+ }
+
+ @Override
+ public int getCount() {
+ return mFilters.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public ContactListFilter getItem(int position) {
+ return mFilters.get(position);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final ContactListFilterView view;
+ if (convertView != null) {
+ view = (ContactListFilterView) convertView;
+ } else {
+ view = (ContactListFilterView) mLayoutInflater.inflate(
+ R.layout.contact_list_filter_item, parent, false);
+ }
+ view.setSingleAccount(mFilters.size() == 1);
+ final ContactListFilter filter = mFilters.get(position);
+ view.setContactListFilter(filter);
+ view.bindView(mAccountTypes);
+ view.setTag(filter);
+ view.setActivated(filter.equals(mCurrentFilter));
+ return view;
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // We have two logical "up" Activities: People and Phone.
+ // Instead of having one static "up" direction, behave like back as an
+ // exceptional case.
+ onBackPressed();
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/com/android/contacts/common/list/ContactListFilterView.java b/src/com/android/contacts/common/list/ContactListFilterView.java
new file mode 100644
index 00000000..4cea7558
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactListFilterView.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2012 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.common.list;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.TextView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+
+/**
+ * Contact list filter parameters.
+ */
+public class ContactListFilterView extends LinearLayout {
+
+ private static final String TAG = ContactListFilterView.class.getSimpleName();
+
+ private ImageView mIcon;
+ private TextView mAccountType;
+ private TextView mAccountUserName;
+ private RadioButton mRadioButton;
+ private ContactListFilter mFilter;
+ private boolean mSingleAccount;
+
+ public ContactListFilterView(Context context) {
+ super(context);
+ }
+
+ public ContactListFilterView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setContactListFilter(ContactListFilter filter) {
+ mFilter = filter;
+ }
+
+ public ContactListFilter getContactListFilter() {
+ return mFilter;
+ }
+
+ public void setSingleAccount(boolean flag) {
+ this.mSingleAccount = flag;
+ }
+
+ @Override
+ public void setActivated(boolean activated) {
+ super.setActivated(activated);
+ if (mRadioButton != null) {
+ mRadioButton.setChecked(activated);
+ } else {
+ // We're guarding against null-pointer exceptions,
+ // but otherwise this code is not expected to work
+ // properly if the button hasn't been initialized.
+ Log.wtf(TAG, "radio-button cannot be activated because it is null");
+ }
+ }
+
+ public void bindView(AccountTypeManager accountTypes) {
+ if (mAccountType == null) {
+ mIcon = (ImageView) findViewById(R.id.icon);
+ mAccountType = (TextView) findViewById(R.id.accountType);
+ mAccountUserName = (TextView) findViewById(R.id.accountUserName);
+ mRadioButton = (RadioButton) findViewById(R.id.radioButton);
+ mRadioButton.setChecked(isActivated());
+ }
+
+ if (mFilter == null) {
+ mAccountType.setText(R.string.contactsList);
+ return;
+ }
+
+ mAccountUserName.setVisibility(View.GONE);
+ switch (mFilter.filterType) {
+ case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: {
+ bindView(0, R.string.list_filter_all_accounts);
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_STARRED: {
+ bindView(R.drawable.ic_menu_star_holo_light, R.string.list_filter_all_starred);
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_CUSTOM: {
+ bindView(R.drawable.ic_menu_settings_holo_light, R.string.list_filter_customize);
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: {
+ bindView(0, R.string.list_filter_phones);
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT: {
+ bindView(0, R.string.list_filter_single);
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_ACCOUNT: {
+ mAccountUserName.setVisibility(View.VISIBLE);
+ mIcon.setVisibility(View.VISIBLE);
+ if (mFilter.icon != null) {
+ mIcon.setImageDrawable(mFilter.icon);
+ } else {
+ mIcon.setImageResource(R.drawable.unknown_source);
+ }
+ final AccountType accountType =
+ accountTypes.getAccountType(mFilter.accountType, mFilter.dataSet);
+ mAccountUserName.setText(mFilter.accountName);
+ mAccountType.setText(accountType.getDisplayLabel(getContext()));
+ break;
+ }
+ }
+ }
+
+ private void bindView(int iconResource, int textResource) {
+ if (iconResource != 0) {
+ mIcon.setVisibility(View.VISIBLE);
+ mIcon.setImageResource(iconResource);
+ } else {
+ mIcon.setVisibility(View.GONE);
+ }
+
+ mAccountType.setText(textResource);
+ }
+}
diff --git a/src/com/android/contacts/common/list/CustomContactListFilterActivity.java b/src/com/android/contacts/common/list/CustomContactListFilterActivity.java
new file mode 100644
index 00000000..feb7df23
--- /dev/null
+++ b/src/com/android/contacts/common/list/CustomContactListFilterActivity.java
@@ -0,0 +1,923 @@
+/*
+ * Copyright (C) 2009 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.common.list;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.app.ProgressDialog;
+import android.content.AsyncTaskLoader;
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.OperationApplicationException;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.Settings;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.CheckBox;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+import android.widget.TextView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.util.EmptyService;
+import com.android.contacts.common.util.LocalizedNameResolver;
+import com.android.contacts.common.util.WeakAsyncTask;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+
+/**
+ * Shows a list of all available {@link Groups} available, letting the user
+ * select which ones they want to be visible.
+ */
+public class CustomContactListFilterActivity extends Activity
+ implements View.OnClickListener, ExpandableListView.OnChildClickListener,
+ LoaderCallbacks<CustomContactListFilterActivity.AccountSet>
+{
+ private static final String TAG = "CustomContactListFilterActivity";
+
+ private static final int ACCOUNT_SET_LOADER_ID = 1;
+
+ private ExpandableListView mList;
+ private DisplayAdapter mAdapter;
+
+ private SharedPreferences mPrefs;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.contact_list_filter_custom);
+
+ mList = (ExpandableListView) findViewById(android.R.id.list);
+ mList.setOnChildClickListener(this);
+ mList.setHeaderDividersEnabled(true);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ mAdapter = new DisplayAdapter(this);
+
+ final LayoutInflater inflater = getLayoutInflater();
+
+ findViewById(R.id.btn_done).setOnClickListener(this);
+ findViewById(R.id.btn_discard).setOnClickListener(this);
+
+ mList.setOnCreateContextMenuListener(this);
+
+ mList.setAdapter(mAdapter);
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ // android.R.id.home will be triggered in onOptionsItemSelected()
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ public static class CustomFilterConfigurationLoader extends AsyncTaskLoader<AccountSet> {
+
+ private AccountSet mAccountSet;
+
+ public CustomFilterConfigurationLoader(Context context) {
+ super(context);
+ }
+
+ @Override
+ public AccountSet loadInBackground() {
+ Context context = getContext();
+ final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context);
+ final ContentResolver resolver = context.getContentResolver();
+
+ final AccountSet accounts = new AccountSet();
+ for (AccountWithDataSet account : accountTypes.getAccounts(false)) {
+ final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
+ if (accountType.isExtension() && !account.hasData(context)) {
+ // Extension with no data -- skip.
+ continue;
+ }
+
+ AccountDisplay accountDisplay =
+ new AccountDisplay(resolver, account.name, account.type, account.dataSet);
+
+ final Uri.Builder groupsUri = Groups.CONTENT_URI.buildUpon()
+ .appendQueryParameter(Groups.ACCOUNT_NAME, account.name)
+ .appendQueryParameter(Groups.ACCOUNT_TYPE, account.type);
+ if (account.dataSet != null) {
+ groupsUri.appendQueryParameter(Groups.DATA_SET, account.dataSet).build();
+ }
+ android.content.EntityIterator iterator =
+ ContactsContract.Groups.newEntityIterator(resolver.query(
+ groupsUri.build(), null, null, null, null));
+ try {
+ boolean hasGroups = false;
+
+ // Create entries for each known group
+ while (iterator.hasNext()) {
+ final ContentValues values = iterator.next().getEntityValues();
+ final GroupDelta group = GroupDelta.fromBefore(values);
+ accountDisplay.addGroup(group);
+ hasGroups = true;
+ }
+ // Create single entry handling ungrouped status
+ accountDisplay.mUngrouped =
+ GroupDelta.fromSettings(resolver, account.name, account.type,
+ account.dataSet, hasGroups);
+ accountDisplay.addGroup(accountDisplay.mUngrouped);
+ } finally {
+ iterator.close();
+ }
+
+ accounts.add(accountDisplay);
+ }
+
+ return accounts;
+ }
+
+ @Override
+ public void deliverResult(AccountSet cursor) {
+ if (isReset()) {
+ return;
+ }
+
+ mAccountSet = cursor;
+
+ if (isStarted()) {
+ super.deliverResult(cursor);
+ }
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mAccountSet != null) {
+ deliverResult(mAccountSet);
+ }
+ if (takeContentChanged() || mAccountSet == null) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+ onStopLoading();
+ mAccountSet = null;
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ getLoaderManager().initLoader(ACCOUNT_SET_LOADER_ID, null, this);
+ super.onStart();
+ }
+
+ @Override
+ public Loader<AccountSet> onCreateLoader(int id, Bundle args) {
+ return new CustomFilterConfigurationLoader(this);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<AccountSet> loader, AccountSet data) {
+ mAdapter.setAccounts(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<AccountSet> loader) {
+ mAdapter.setAccounts(null);
+ }
+
+ private static final int DEFAULT_SHOULD_SYNC = 1;
+ private static final int DEFAULT_VISIBLE = 0;
+
+ /**
+ * Entry holding any changes to {@link Groups} or {@link Settings} rows,
+ * such as {@link Groups#SHOULD_SYNC} or {@link Groups#GROUP_VISIBLE}.
+ */
+ protected static class GroupDelta extends ValuesDelta {
+ private boolean mUngrouped = false;
+ private boolean mAccountHasGroups;
+
+ private GroupDelta() {
+ super();
+ }
+
+ /**
+ * Build {@link GroupDelta} from the {@link Settings} row for the given
+ * {@link Settings#ACCOUNT_NAME}, {@link Settings#ACCOUNT_TYPE}, and
+ * {@link Settings#DATA_SET}.
+ */
+ public static GroupDelta fromSettings(ContentResolver resolver, String accountName,
+ String accountType, String dataSet, boolean accountHasGroups) {
+ final Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon()
+ .appendQueryParameter(Settings.ACCOUNT_NAME, accountName)
+ .appendQueryParameter(Settings.ACCOUNT_TYPE, accountType);
+ if (dataSet != null) {
+ settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet);
+ }
+ final Cursor cursor = resolver.query(settingsUri.build(), new String[] {
+ Settings.SHOULD_SYNC, Settings.UNGROUPED_VISIBLE
+ }, null, null, null);
+
+ try {
+ final ContentValues values = new ContentValues();
+ values.put(Settings.ACCOUNT_NAME, accountName);
+ values.put(Settings.ACCOUNT_TYPE, accountType);
+ values.put(Settings.DATA_SET, dataSet);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ // Read existing values when present
+ values.put(Settings.SHOULD_SYNC, cursor.getInt(0));
+ values.put(Settings.UNGROUPED_VISIBLE, cursor.getInt(1));
+ return fromBefore(values).setUngrouped(accountHasGroups);
+ } else {
+ // Nothing found, so treat as create
+ values.put(Settings.SHOULD_SYNC, DEFAULT_SHOULD_SYNC);
+ values.put(Settings.UNGROUPED_VISIBLE, DEFAULT_VISIBLE);
+ return fromAfter(values).setUngrouped(accountHasGroups);
+ }
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ }
+
+ public static GroupDelta fromBefore(ContentValues before) {
+ final GroupDelta entry = new GroupDelta();
+ entry.mBefore = before;
+ entry.mAfter = new ContentValues();
+ return entry;
+ }
+
+ public static GroupDelta fromAfter(ContentValues after) {
+ final GroupDelta entry = new GroupDelta();
+ entry.mBefore = null;
+ entry.mAfter = after;
+ return entry;
+ }
+
+ protected GroupDelta setUngrouped(boolean accountHasGroups) {
+ mUngrouped = true;
+ mAccountHasGroups = accountHasGroups;
+ return this;
+ }
+
+ @Override
+ public boolean beforeExists() {
+ return mBefore != null;
+ }
+
+ public boolean getShouldSync() {
+ return getAsInteger(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC,
+ DEFAULT_SHOULD_SYNC) != 0;
+ }
+
+ public boolean getVisible() {
+ return getAsInteger(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE,
+ DEFAULT_VISIBLE) != 0;
+ }
+
+ public void putShouldSync(boolean shouldSync) {
+ put(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC, shouldSync ? 1 : 0);
+ }
+
+ public void putVisible(boolean visible) {
+ put(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, visible ? 1 : 0);
+ }
+
+ private String getAccountType() {
+ return (mBefore == null ? mAfter : mBefore).getAsString(Settings.ACCOUNT_TYPE);
+ }
+
+ public CharSequence getTitle(Context context) {
+ if (mUngrouped) {
+ final String customAllContactsName =
+ LocalizedNameResolver.getAllContactsName(context, getAccountType());
+ if (customAllContactsName != null) {
+ return customAllContactsName;
+ }
+ if (mAccountHasGroups) {
+ return context.getText(R.string.display_ungrouped);
+ } else {
+ return context.getText(R.string.display_all_contacts);
+ }
+ } else {
+ final Integer titleRes = getAsInteger(Groups.TITLE_RES);
+ if (titleRes != null) {
+ final String packageName = getAsString(Groups.RES_PACKAGE);
+ return context.getPackageManager().getText(packageName, titleRes, null);
+ } else {
+ return getAsString(Groups.TITLE);
+ }
+ }
+ }
+
+ /**
+ * Build a possible {@link ContentProviderOperation} to persist any
+ * changes to the {@link Groups} or {@link Settings} row described by
+ * this {@link GroupDelta}.
+ */
+ public ContentProviderOperation buildDiff() {
+ if (isInsert()) {
+ // Only allow inserts for Settings
+ if (mUngrouped) {
+ mAfter.remove(mIdColumn);
+ return ContentProviderOperation.newInsert(Settings.CONTENT_URI)
+ .withValues(mAfter)
+ .build();
+ }
+ else {
+ throw new IllegalStateException("Unexpected diff");
+ }
+ } else if (isUpdate()) {
+ if (mUngrouped) {
+ String accountName = this.getAsString(Settings.ACCOUNT_NAME);
+ String accountType = this.getAsString(Settings.ACCOUNT_TYPE);
+ String dataSet = this.getAsString(Settings.DATA_SET);
+ StringBuilder selection = new StringBuilder(Settings.ACCOUNT_NAME + "=? AND "
+ + Settings.ACCOUNT_TYPE + "=?");
+ String[] selectionArgs;
+ if (dataSet == null) {
+ selection.append(" AND " + Settings.DATA_SET + " IS NULL");
+ selectionArgs = new String[] {accountName, accountType};
+ } else {
+ selection.append(" AND " + Settings.DATA_SET + "=?");
+ selectionArgs = new String[] {accountName, accountType, dataSet};
+ }
+ return ContentProviderOperation.newUpdate(Settings.CONTENT_URI)
+ .withSelection(selection.toString(), selectionArgs)
+ .withValues(mAfter)
+ .build();
+ } else {
+ return ContentProviderOperation.newUpdate(
+ addCallerIsSyncAdapterParameter(Groups.CONTENT_URI))
+ .withSelection(Groups._ID + "=" + this.getId(), null)
+ .withValues(mAfter)
+ .build();
+ }
+ } else {
+ return null;
+ }
+ }
+ }
+
+ private static Uri addCallerIsSyncAdapterParameter(Uri uri) {
+ return uri.buildUpon()
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build();
+ }
+
+ /**
+ * {@link Comparator} to sort by {@link Groups#_ID}.
+ */
+ private static Comparator<GroupDelta> sIdComparator = new Comparator<GroupDelta>() {
+ public int compare(GroupDelta object1, GroupDelta object2) {
+ final Long id1 = object1.getId();
+ final Long id2 = object2.getId();
+ if (id1 == null && id2 == null) {
+ return 0;
+ } else if (id1 == null) {
+ return -1;
+ } else if (id2 == null) {
+ return 1;
+ } else if (id1 < id2) {
+ return -1;
+ } else if (id1 > id2) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ };
+
+ /**
+ * Set of all {@link AccountDisplay} entries, one for each source.
+ */
+ protected static class AccountSet extends ArrayList<AccountDisplay> {
+ public ArrayList<ContentProviderOperation> buildDiff() {
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ for (AccountDisplay account : this) {
+ account.buildDiff(diff);
+ }
+ return diff;
+ }
+ }
+
+ /**
+ * {@link GroupDelta} details for a single {@link AccountWithDataSet}, usually shown as
+ * children under a single expandable group.
+ */
+ protected static class AccountDisplay {
+ public final String mName;
+ public final String mType;
+ public final String mDataSet;
+
+ public GroupDelta mUngrouped;
+ public ArrayList<GroupDelta> mSyncedGroups = Lists.newArrayList();
+ public ArrayList<GroupDelta> mUnsyncedGroups = Lists.newArrayList();
+
+ /**
+ * Build an {@link AccountDisplay} covering all {@link Groups} under the
+ * given {@link AccountWithDataSet}.
+ */
+ public AccountDisplay(ContentResolver resolver, String accountName, String accountType,
+ String dataSet) {
+ mName = accountName;
+ mType = accountType;
+ mDataSet = dataSet;
+ }
+
+ /**
+ * Add the given {@link GroupDelta} internally, filing based on its
+ * {@link GroupDelta#getShouldSync()} status.
+ */
+ private void addGroup(GroupDelta group) {
+ if (group.getShouldSync()) {
+ mSyncedGroups.add(group);
+ } else {
+ mUnsyncedGroups.add(group);
+ }
+ }
+
+ /**
+ * Set the {@link GroupDelta#putShouldSync(boolean)} value for all
+ * children {@link GroupDelta} rows.
+ */
+ public void setShouldSync(boolean shouldSync) {
+ final Iterator<GroupDelta> oppositeChildren = shouldSync ?
+ mUnsyncedGroups.iterator() : mSyncedGroups.iterator();
+ while (oppositeChildren.hasNext()) {
+ final GroupDelta child = oppositeChildren.next();
+ setShouldSync(child, shouldSync, false);
+ oppositeChildren.remove();
+ }
+ }
+
+ public void setShouldSync(GroupDelta child, boolean shouldSync) {
+ setShouldSync(child, shouldSync, true);
+ }
+
+ /**
+ * Set {@link GroupDelta#putShouldSync(boolean)}, and file internally
+ * based on updated state.
+ */
+ public void setShouldSync(GroupDelta child, boolean shouldSync, boolean attemptRemove) {
+ child.putShouldSync(shouldSync);
+ if (shouldSync) {
+ if (attemptRemove) {
+ mUnsyncedGroups.remove(child);
+ }
+ mSyncedGroups.add(child);
+ Collections.sort(mSyncedGroups, sIdComparator);
+ } else {
+ if (attemptRemove) {
+ mSyncedGroups.remove(child);
+ }
+ mUnsyncedGroups.add(child);
+ }
+ }
+
+ /**
+ * Build set of {@link ContentProviderOperation} to persist any user
+ * changes to {@link GroupDelta} rows under this {@link AccountWithDataSet}.
+ */
+ public void buildDiff(ArrayList<ContentProviderOperation> diff) {
+ for (GroupDelta group : mSyncedGroups) {
+ final ContentProviderOperation oper = group.buildDiff();
+ if (oper != null) diff.add(oper);
+ }
+ for (GroupDelta group : mUnsyncedGroups) {
+ final ContentProviderOperation oper = group.buildDiff();
+ if (oper != null) diff.add(oper);
+ }
+ }
+ }
+
+ /**
+ * {@link ExpandableListAdapter} that shows {@link GroupDelta} settings,
+ * grouped by {@link AccountWithDataSet} type. Shows footer row when any groups are
+ * unsynced, as determined through {@link AccountDisplay#mUnsyncedGroups}.
+ */
+ protected static class DisplayAdapter extends BaseExpandableListAdapter {
+ private Context mContext;
+ private LayoutInflater mInflater;
+ private AccountTypeManager mAccountTypes;
+ private AccountSet mAccounts;
+
+ private boolean mChildWithPhones = false;
+
+ public DisplayAdapter(Context context) {
+ mContext = context;
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mAccountTypes = AccountTypeManager.getInstance(context);
+ }
+
+ public void setAccounts(AccountSet accounts) {
+ mAccounts = accounts;
+ notifyDataSetChanged();
+ }
+
+ /**
+ * In group descriptions, show the number of contacts with phone
+ * numbers, in addition to the total contacts.
+ */
+ public void setChildDescripWithPhones(boolean withPhones) {
+ mChildWithPhones = withPhones;
+ }
+
+ @Override
+ public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+ ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(
+ R.layout.custom_contact_list_filter_account, parent, false);
+ }
+
+ final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+ final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
+
+ final AccountDisplay account = (AccountDisplay)this.getGroup(groupPosition);
+
+ final AccountType accountType = mAccountTypes.getAccountType(
+ account.mType, account.mDataSet);
+
+ text1.setText(account.mName);
+ text1.setVisibility(account.mName == null ? View.GONE : View.VISIBLE);
+ text2.setText(accountType.getDisplayLabel(mContext));
+
+ return convertView;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+ View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(
+ R.layout.custom_contact_list_filter_group, parent, false);
+ }
+
+ final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+ final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
+ final CheckBox checkbox = (CheckBox)convertView.findViewById(android.R.id.checkbox);
+
+ final AccountDisplay account = mAccounts.get(groupPosition);
+ final GroupDelta child = (GroupDelta)this.getChild(groupPosition, childPosition);
+ if (child != null) {
+ // Handle normal group, with title and checkbox
+ final boolean groupVisible = child.getVisible();
+ checkbox.setVisibility(View.VISIBLE);
+ checkbox.setChecked(groupVisible);
+
+ final CharSequence groupTitle = child.getTitle(mContext);
+ text1.setText(groupTitle);
+ text2.setVisibility(View.GONE);
+ } else {
+ // When unknown child, this is "more" footer view
+ checkbox.setVisibility(View.GONE);
+ text1.setText(R.string.display_more_groups);
+ text2.setVisibility(View.GONE);
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ final AccountDisplay account = mAccounts.get(groupPosition);
+ final boolean validChild = childPosition >= 0
+ && childPosition < account.mSyncedGroups.size();
+ if (validChild) {
+ return account.mSyncedGroups.get(childPosition);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ final GroupDelta child = (GroupDelta)getChild(groupPosition, childPosition);
+ if (child != null) {
+ final Long childId = child.getId();
+ return childId != null ? childId : Long.MIN_VALUE;
+ } else {
+ return Long.MIN_VALUE;
+ }
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ // Count is any synced groups, plus possible footer
+ final AccountDisplay account = mAccounts.get(groupPosition);
+ final boolean anyHidden = account.mUnsyncedGroups.size() > 0;
+ return account.mSyncedGroups.size() + (anyHidden ? 1 : 0);
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return mAccounts.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ if (mAccounts == null) {
+ return 0;
+ }
+ return mAccounts.size();
+ }
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return groupPosition;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+ }
+
+ /** {@inheritDoc} */
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.btn_done: {
+ this.doSaveAction();
+ break;
+ }
+ case R.id.btn_discard: {
+ this.finish();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle any clicks on {@link ExpandableListAdapter} children, which
+ * usually mean toggling its visible state.
+ */
+ @Override
+ public boolean onChildClick(ExpandableListView parent, View view, int groupPosition,
+ int childPosition, long id) {
+ final CheckBox checkbox = (CheckBox)view.findViewById(android.R.id.checkbox);
+
+ final AccountDisplay account = (AccountDisplay)mAdapter.getGroup(groupPosition);
+ final GroupDelta child = (GroupDelta)mAdapter.getChild(groupPosition, childPosition);
+ if (child != null) {
+ checkbox.toggle();
+ child.putVisible(checkbox.isChecked());
+ } else {
+ // Open context menu for bringing back unsynced
+ this.openContextMenu(view);
+ }
+ return true;
+ }
+
+ // TODO: move these definitions to framework constants when we begin
+ // defining this mode through <sync-adapter> tags
+ private static final int SYNC_MODE_UNSUPPORTED = 0;
+ private static final int SYNC_MODE_UNGROUPED = 1;
+ private static final int SYNC_MODE_EVERYTHING = 2;
+
+ protected int getSyncMode(AccountDisplay account) {
+ // TODO: read sync mode through <sync-adapter> definition
+ if (GoogleAccountType.ACCOUNT_TYPE.equals(account.mType) && account.mDataSet == null) {
+ return SYNC_MODE_EVERYTHING;
+ } else {
+ return SYNC_MODE_UNSUPPORTED;
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view,
+ ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ // Bail if not working with expandable long-press, or if not child
+ if (!(menuInfo instanceof ExpandableListContextMenuInfo)) return;
+
+ final ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo;
+ final int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
+ final int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition);
+
+ // Skip long-press on expandable parents
+ if (childPosition == -1) return;
+
+ final AccountDisplay account = (AccountDisplay)mAdapter.getGroup(groupPosition);
+ final GroupDelta child = (GroupDelta)mAdapter.getChild(groupPosition, childPosition);
+
+ // Ignore when selective syncing unsupported
+ final int syncMode = getSyncMode(account);
+ if (syncMode == SYNC_MODE_UNSUPPORTED) return;
+
+ if (child != null) {
+ showRemoveSync(menu, account, child, syncMode);
+ } else {
+ showAddSync(menu, account, syncMode);
+ }
+ }
+
+ protected void showRemoveSync(ContextMenu menu, final AccountDisplay account,
+ final GroupDelta child, final int syncMode) {
+ final CharSequence title = child.getTitle(this);
+
+ menu.setHeaderTitle(title);
+ menu.add(R.string.menu_sync_remove).setOnMenuItemClickListener(
+ new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ handleRemoveSync(account, child, syncMode, title);
+ return true;
+ }
+ });
+ }
+
+ protected void handleRemoveSync(final AccountDisplay account, final GroupDelta child,
+ final int syncMode, CharSequence title) {
+ final boolean shouldSyncUngrouped = account.mUngrouped.getShouldSync();
+ if (syncMode == SYNC_MODE_EVERYTHING && shouldSyncUngrouped
+ && !child.equals(account.mUngrouped)) {
+ // Warn before removing this group when it would cause ungrouped to stop syncing
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ final CharSequence removeMessage = this.getString(
+ R.string.display_warn_remove_ungrouped, title);
+ builder.setTitle(R.string.menu_sync_remove);
+ builder.setMessage(removeMessage);
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // Mark both this group and ungrouped to stop syncing
+ account.setShouldSync(account.mUngrouped, false);
+ account.setShouldSync(child, false);
+ mAdapter.notifyDataSetChanged();
+ }
+ });
+ builder.show();
+ } else {
+ // Mark this group to not sync
+ account.setShouldSync(child, false);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ protected void showAddSync(ContextMenu menu, final AccountDisplay account, final int syncMode) {
+ menu.setHeaderTitle(R.string.dialog_sync_add);
+
+ // Create item for each available, unsynced group
+ for (final GroupDelta child : account.mUnsyncedGroups) {
+ if (!child.getShouldSync()) {
+ final CharSequence title = child.getTitle(this);
+ menu.add(title).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // Adding specific group for syncing
+ if (child.mUngrouped && syncMode == SYNC_MODE_EVERYTHING) {
+ account.setShouldSync(true);
+ } else {
+ account.setShouldSync(child, true);
+ }
+ mAdapter.notifyDataSetChanged();
+ return true;
+ }
+ });
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void doSaveAction() {
+ if (mAdapter == null || mAdapter.mAccounts == null) {
+ finish();
+ return;
+ }
+
+ setResult(RESULT_OK);
+
+ final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff();
+ if (diff.isEmpty()) {
+ finish();
+ return;
+ }
+
+ new UpdateTask(this).execute(diff);
+ }
+
+ /**
+ * Background task that persists changes to {@link Groups#GROUP_VISIBLE},
+ * showing spinner dialog to user while updating.
+ */
+ public static class UpdateTask extends
+ WeakAsyncTask<ArrayList<ContentProviderOperation>, Void, Void, Activity> {
+ private ProgressDialog mProgress;
+
+ public UpdateTask(Activity target) {
+ super(target);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void onPreExecute(Activity target) {
+ final Context context = target;
+
+ mProgress = ProgressDialog.show(
+ context, null, context.getText(R.string.savingDisplayGroups));
+
+ // Before starting this task, start an empty service to protect our
+ // process from being reclaimed by the system.
+ context.startService(new Intent(context, EmptyService.class));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Void doInBackground(
+ Activity target, ArrayList<ContentProviderOperation>... params) {
+ final Context context = target;
+ final ContentValues values = new ContentValues();
+ final ContentResolver resolver = context.getContentResolver();
+
+ try {
+ final ArrayList<ContentProviderOperation> diff = params[0];
+ resolver.applyBatch(ContactsContract.AUTHORITY, diff);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Problem saving display groups", e);
+ } catch (OperationApplicationException e) {
+ Log.e(TAG, "Problem saving display groups", e);
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void onPostExecute(Activity target, Void result) {
+ final Context context = target;
+
+ try {
+ mProgress.dismiss();
+ } catch (Exception e) {
+ Log.e(TAG, "Error dismissing progress dialog", e);
+ }
+
+ target.finish();
+
+ // Stop the service that was protecting us
+ context.stopService(new Intent(context, EmptyService.class));
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // Pretend cancel.
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/com/android/contacts/common/list/ShortcutIntentBuilder.java b/src/com/android/contacts/common/list/ShortcutIntentBuilder.java
new file mode 100644
index 00000000..4ac06644
--- /dev/null
+++ b/src/com/android/contacts/common/list/ShortcutIntentBuilder.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2010 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.common.list;
+
+import android.app.ActivityManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.R;
+
+/**
+ * Constructs shortcut intents.
+ */
+public class ShortcutIntentBuilder {
+
+ private static final String[] CONTACT_COLUMNS = {
+ Contacts.DISPLAY_NAME,
+ Contacts.PHOTO_ID,
+ };
+
+ private static final int CONTACT_DISPLAY_NAME_COLUMN_INDEX = 0;
+ private static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 1;
+
+ private static final String[] PHONE_COLUMNS = {
+ Phone.DISPLAY_NAME,
+ Phone.PHOTO_ID,
+ Phone.NUMBER,
+ Phone.TYPE,
+ Phone.LABEL
+ };
+
+ private static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 0;
+ private static final int PHONE_PHOTO_ID_COLUMN_INDEX = 1;
+ private static final int PHONE_NUMBER_COLUMN_INDEX = 2;
+ private static final int PHONE_TYPE_COLUMN_INDEX = 3;
+ private static final int PHONE_LABEL_COLUMN_INDEX = 4;
+
+ private static final String[] PHOTO_COLUMNS = {
+ Photo.PHOTO,
+ };
+
+ private static final int PHOTO_PHOTO_COLUMN_INDEX = 0;
+
+ private static final String PHOTO_SELECTION = Photo._ID + "=?";
+
+ private final OnShortcutIntentCreatedListener mListener;
+ private final Context mContext;
+ private int mIconSize;
+ private final int mIconDensity;
+ private final int mBorderWidth;
+ private final int mBorderColor;
+
+ /**
+ * This is a hidden API of the launcher in JellyBean that allows us to disable the animation
+ * that it would usually do, because it interferes with our own animation for QuickContact
+ */
+ public static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION =
+ "com.android.launcher.intent.extra.shortcut.INGORE_LAUNCH_ANIMATION";
+
+ /**
+ * Listener interface.
+ */
+ public interface OnShortcutIntentCreatedListener {
+
+ /**
+ * Callback for shortcut intent creation.
+ *
+ * @param uri the original URI for which the shortcut intent has been
+ * created.
+ * @param shortcutIntent resulting shortcut intent.
+ */
+ void onShortcutIntentCreated(Uri uri, Intent shortcutIntent);
+ }
+
+ public ShortcutIntentBuilder(Context context, OnShortcutIntentCreatedListener listener) {
+ mContext = context;
+ mListener = listener;
+
+ final Resources r = context.getResources();
+ final ActivityManager am = (ActivityManager) context
+ .getSystemService(Context.ACTIVITY_SERVICE);
+ mIconSize = r.getDimensionPixelSize(R.dimen.shortcut_icon_size);
+ if (mIconSize == 0) {
+ mIconSize = am.getLauncherLargeIconSize();
+ }
+ mIconDensity = am.getLauncherLargeIconDensity();
+ mBorderWidth = r.getDimensionPixelOffset(
+ R.dimen.shortcut_icon_border_width);
+ mBorderColor = r.getColor(R.color.shortcut_overlay_text_background);
+ }
+
+ public void createContactShortcutIntent(Uri contactUri) {
+ new ContactLoadingAsyncTask(contactUri).execute();
+ }
+
+ public void createPhoneNumberShortcutIntent(Uri dataUri, String shortcutAction) {
+ new PhoneNumberLoadingAsyncTask(dataUri, shortcutAction).execute();
+ }
+
+ /**
+ * An asynchronous task that loads name, photo and other data from the database.
+ */
+ private abstract class LoadingAsyncTask extends AsyncTask<Void, Void, Void> {
+ protected Uri mUri;
+ protected String mContentType;
+ protected String mDisplayName;
+ protected byte[] mBitmapData;
+ protected long mPhotoId;
+
+ public LoadingAsyncTask(Uri uri) {
+ mUri = uri;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ mContentType = mContext.getContentResolver().getType(mUri);
+ loadData();
+ loadPhoto();
+ return null;
+ }
+
+ protected abstract void loadData();
+
+ private void loadPhoto() {
+ if (mPhotoId == 0) {
+ return;
+ }
+
+ ContentResolver resolver = mContext.getContentResolver();
+ Cursor cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLUMNS, PHOTO_SELECTION,
+ new String[] { String.valueOf(mPhotoId) }, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ mBitmapData = cursor.getBlob(PHOTO_PHOTO_COLUMN_INDEX);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+ }
+
+ private final class ContactLoadingAsyncTask extends LoadingAsyncTask {
+ public ContactLoadingAsyncTask(Uri uri) {
+ super(uri);
+ }
+
+ @Override
+ protected void loadData() {
+ ContentResolver resolver = mContext.getContentResolver();
+ Cursor cursor = resolver.query(mUri, CONTACT_COLUMNS, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ mDisplayName = cursor.getString(CONTACT_DISPLAY_NAME_COLUMN_INDEX);
+ mPhotoId = cursor.getLong(CONTACT_PHOTO_ID_COLUMN_INDEX);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+ @Override
+ protected void onPostExecute(Void result) {
+ createContactShortcutIntent(mUri, mContentType, mDisplayName, mBitmapData);
+ }
+ }
+
+ private final class PhoneNumberLoadingAsyncTask extends LoadingAsyncTask {
+ private final String mShortcutAction;
+ private String mPhoneNumber;
+ private int mPhoneType;
+ private String mPhoneLabel;
+
+ public PhoneNumberLoadingAsyncTask(Uri uri, String shortcutAction) {
+ super(uri);
+ mShortcutAction = shortcutAction;
+ }
+
+ @Override
+ protected void loadData() {
+ ContentResolver resolver = mContext.getContentResolver();
+ Cursor cursor = resolver.query(mUri, PHONE_COLUMNS, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ mDisplayName = cursor.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX);
+ mPhotoId = cursor.getLong(PHONE_PHOTO_ID_COLUMN_INDEX);
+ mPhoneNumber = cursor.getString(PHONE_NUMBER_COLUMN_INDEX);
+ mPhoneType = cursor.getInt(PHONE_TYPE_COLUMN_INDEX);
+ mPhoneLabel = cursor.getString(PHONE_LABEL_COLUMN_INDEX);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ createPhoneNumberShortcutIntent(mUri, mDisplayName, mBitmapData, mPhoneNumber,
+ mPhoneType, mPhoneLabel, mShortcutAction);
+ }
+ }
+
+ private Bitmap getPhotoBitmap(byte[] bitmapData) {
+ Bitmap bitmap;
+ if (bitmapData != null) {
+ bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length, null);
+ } else {
+ bitmap = ((BitmapDrawable) mContext.getResources().getDrawableForDensity(
+ R.drawable.ic_contact_picture_holo_light, mIconDensity)).getBitmap();
+ }
+ return bitmap;
+ }
+
+ private void createContactShortcutIntent(Uri contactUri, String contentType, String displayName,
+ byte[] bitmapData) {
+ Bitmap bitmap = getPhotoBitmap(bitmapData);
+
+ Intent shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT);
+
+ // When starting from the launcher, start in a new, cleared task.
+ // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we
+ // clear the whole thing preemptively here since QuickContactActivity will
+ // finish itself when launching other detail activities.
+ shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+ // Tell the launcher to not do its animation, because we are doing our own
+ shortcutIntent.putExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
+
+ shortcutIntent.setDataAndType(contactUri, contentType);
+ shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_MODE,
+ ContactsContract.QuickContact.MODE_LARGE);
+ shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES,
+ (String[]) null);
+
+ final Bitmap icon = generateQuickContactIcon(bitmap);
+
+ Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ if (TextUtils.isEmpty(displayName)) {
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, mContext.getResources().getString(
+ R.string.missing_name));
+ } else {
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
+ }
+
+ mListener.onShortcutIntentCreated(contactUri, intent);
+ }
+
+ private void createPhoneNumberShortcutIntent(Uri uri, String displayName, byte[] bitmapData,
+ String phoneNumber, int phoneType, String phoneLabel, String shortcutAction) {
+ Bitmap bitmap = getPhotoBitmap(bitmapData);
+
+ Uri phoneUri;
+ if (Intent.ACTION_CALL.equals(shortcutAction)) {
+ // Make the URI a direct tel: URI so that it will always continue to work
+ phoneUri = Uri.fromParts(CallUtil.SCHEME_TEL, phoneNumber, null);
+ bitmap = generatePhoneNumberIcon(bitmap, phoneType, phoneLabel,
+ R.drawable.badge_action_call);
+ } else {
+ phoneUri = Uri.fromParts(CallUtil.SCHEME_SMSTO, phoneNumber, null);
+ bitmap = generatePhoneNumberIcon(bitmap, phoneType, phoneLabel,
+ R.drawable.badge_action_sms);
+ }
+
+ Intent shortcutIntent = new Intent(shortcutAction, phoneUri);
+ shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bitmap);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
+
+ mListener.onShortcutIntentCreated(uri, intent);
+ }
+
+ private void drawBorder(Canvas canvas, Rect dst) {
+ // Darken the border
+ final Paint workPaint = new Paint();
+ workPaint.setColor(mBorderColor);
+ workPaint.setStyle(Paint.Style.STROKE);
+ // The stroke is drawn centered on the rect bounds, and since half will be drawn outside the
+ // bounds, we need to double the width for it to appear as intended.
+ workPaint.setStrokeWidth(mBorderWidth * 2);
+ canvas.drawRect(dst, workPaint);
+ }
+
+ private Bitmap generateQuickContactIcon(Bitmap photo) {
+
+ // Setup the drawing classes
+ Bitmap icon = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(icon);
+
+ // Copy in the photo
+ Paint photoPaint = new Paint();
+ photoPaint.setDither(true);
+ photoPaint.setFilterBitmap(true);
+ Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
+ Rect dst = new Rect(0,0, mIconSize, mIconSize);
+ canvas.drawBitmap(photo, src, dst, photoPaint);
+
+ drawBorder(canvas, dst);
+
+ Drawable overlay = mContext.getResources().getDrawableForDensity(
+ com.android.internal.R.drawable.quickcontact_badge_overlay_dark, mIconDensity);
+
+ overlay.setBounds(dst);
+ overlay.draw(canvas);
+ canvas.setBitmap(null);
+
+ return icon;
+ }
+
+ /**
+ * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
+ * number, and if there is a photo also adds the call action icon.
+ */
+ private Bitmap generatePhoneNumberIcon(Bitmap photo, int phoneType, String phoneLabel,
+ int actionResId) {
+ final Resources r = mContext.getResources();
+ final float density = r.getDisplayMetrics().density;
+
+ Bitmap phoneIcon = ((BitmapDrawable) r.getDrawableForDensity(actionResId, mIconDensity))
+ .getBitmap();
+
+ // Setup the drawing classes
+ Bitmap icon = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(icon);
+
+ // Copy in the photo
+ Paint photoPaint = new Paint();
+ photoPaint.setDither(true);
+ photoPaint.setFilterBitmap(true);
+ Rect src = new Rect(0, 0, photo.getWidth(), photo.getHeight());
+ Rect dst = new Rect(0, 0, mIconSize, mIconSize);
+ canvas.drawBitmap(photo, src, dst, photoPaint);
+
+ drawBorder(canvas, dst);
+
+ // Create an overlay for the phone number type
+ CharSequence overlay = Phone.getTypeLabel(r, phoneType, phoneLabel);
+
+ if (overlay != null) {
+ TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
+ textPaint.setTextSize(r.getDimension(R.dimen.shortcut_overlay_text_size));
+ textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
+ textPaint.setShadowLayer(4f, 0, 2f, r.getColor(R.color.textColorIconOverlayShadow));
+
+ final FontMetricsInt fmi = textPaint.getFontMetricsInt();
+
+ // First fill in a darker background around the text to be drawn
+ final Paint workPaint = new Paint();
+ workPaint.setColor(mBorderColor);
+ workPaint.setStyle(Paint.Style.FILL);
+ final int textPadding = r
+ .getDimensionPixelOffset(R.dimen.shortcut_overlay_text_background_padding);
+ final int textBandHeight = (fmi.descent - fmi.ascent) + textPadding * 2;
+ dst.set(0 + mBorderWidth, mIconSize - textBandHeight, mIconSize - mBorderWidth,
+ mIconSize - mBorderWidth);
+ canvas.drawRect(dst, workPaint);
+
+ final float sidePadding = mBorderWidth;
+ overlay = TextUtils.ellipsize(overlay, textPaint, mIconSize - 2 * sidePadding,
+ TruncateAt.END_SMALL);
+ final float textWidth = textPaint.measureText(overlay, 0, overlay.length());
+ canvas.drawText(overlay, 0, overlay.length(), (mIconSize - textWidth) / 2, mIconSize
+ - fmi.descent - textPadding, textPaint);
+ }
+
+ // Draw the phone action icon as an overlay
+ src.set(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
+ int iconWidth = icon.getWidth();
+ dst.set(iconWidth - ((int) (20 * density)), -1,
+ iconWidth, ((int) (19 * density)));
+ dst.offset(-mBorderWidth, mBorderWidth);
+ canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
+
+ canvas.setBitmap(null);
+
+ return icon;
+ }
+}
diff --git a/src/com/android/contacts/common/util/AccountFilterUtil.java b/src/com/android/contacts/common/util/AccountFilterUtil.java
new file mode 100644
index 00000000..d1820c85
--- /dev/null
+++ b/src/com/android/contacts/common/util/AccountFilterUtil.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2012 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.common.util;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.list.AccountFilterActivity;
+import com.android.contacts.common.list.ContactListFilter;
+import com.android.contacts.common.list.ContactListFilterController;
+
+/**
+ * Utility class for account filter manipulation.
+ */
+public class AccountFilterUtil {
+ private static final String TAG = AccountFilterUtil.class.getSimpleName();
+
+ /**
+ * Find TextView with the id "account_filter_header" and set correct text for the account
+ * filter header.
+ *
+ * @param filterContainer View containing TextView with id "account_filter_header"
+ * @return true when header text is set in the call. You may use this for conditionally
+ * showing or hiding this entire view.
+ */
+ public static boolean updateAccountFilterTitleForPeople(View filterContainer,
+ ContactListFilter filter, boolean showTitleForAllAccounts) {
+ return updateAccountFilterTitle(filterContainer, filter, showTitleForAllAccounts, false);
+ }
+
+ /**
+ * Similar to {@link #updateAccountFilterTitleForPeople(View, ContactListFilter, boolean,
+ * boolean)}, but for Phone UI.
+ */
+ public static boolean updateAccountFilterTitleForPhone(View filterContainer,
+ ContactListFilter filter, boolean showTitleForAllAccounts) {
+ return updateAccountFilterTitle(
+ filterContainer, filter, showTitleForAllAccounts, true);
+ }
+
+ private static boolean updateAccountFilterTitle(View filterContainer,
+ ContactListFilter filter, boolean showTitleForAllAccounts,
+ boolean forPhone) {
+ final Context context = filterContainer.getContext();
+ final TextView headerTextView = (TextView)
+ filterContainer.findViewById(R.id.account_filter_header);
+
+ boolean textWasSet = false;
+ if (filter != null) {
+ if (forPhone) {
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) {
+ if (showTitleForAllAccounts) {
+ headerTextView.setText(R.string.list_filter_phones);
+ textWasSet = true;
+ }
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
+ headerTextView.setText(context.getString(
+ R.string.listAllContactsInAccount, filter.accountName));
+ textWasSet = true;
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+ headerTextView.setText(R.string.listCustomView);
+ textWasSet = true;
+ } else {
+ Log.w(TAG, "Filter type \"" + filter.filterType + "\" isn't expected.");
+ }
+ } else {
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) {
+ if (showTitleForAllAccounts) {
+ headerTextView.setText(R.string.list_filter_all_accounts);
+ textWasSet = true;
+ }
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
+ headerTextView.setText(context.getString(
+ R.string.listAllContactsInAccount, filter.accountName));
+ textWasSet = true;
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+ headerTextView.setText(R.string.listCustomView);
+ textWasSet = true;
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
+ headerTextView.setText(R.string.listSingleContact);
+ textWasSet = true;
+ } else {
+ Log.w(TAG, "Filter type \"" + filter.filterType + "\" isn't expected.");
+ }
+ }
+ } else {
+ Log.w(TAG, "Filter is null.");
+ }
+ return textWasSet;
+ }
+
+ /**
+ * Launches account filter setting Activity using
+ * {@link Activity#startActivityForResult(Intent, int)}.
+ *
+ * @param activity
+ * @param requestCode requestCode for {@link Activity#startActivityForResult(Intent, int)}
+ * @param currentFilter currently-selected filter, so that it can be displayed as activated.
+ */
+ public static void startAccountFilterActivityForResult(
+ Activity activity, int requestCode, ContactListFilter currentFilter) {
+ final Intent intent = new Intent(activity, AccountFilterActivity.class);
+ intent.putExtra(AccountFilterActivity.KEY_EXTRA_CURRENT_FILTER, currentFilter);
+ activity.startActivityForResult(intent, requestCode);
+ }
+
+ /**
+ * Very similar to
+ * {@link #startAccountFilterActivityForResult(Activity, int, ContactListFilter)}
+ * but uses Fragment instead.
+ */
+ public static void startAccountFilterActivityForResult(
+ Fragment fragment, int requestCode, ContactListFilter currentFilter) {
+ final Activity activity = fragment.getActivity();
+ if (activity != null) {
+ final Intent intent = new Intent(activity, AccountFilterActivity.class);
+ intent.putExtra(AccountFilterActivity.KEY_EXTRA_CURRENT_FILTER, currentFilter);
+ fragment.startActivityForResult(intent, requestCode);
+ } else {
+ Log.w(TAG, "getActivity() returned null. Ignored");
+ }
+ }
+
+ /**
+ * Useful method to handle onActivityResult() for
+ * {@link #startAccountFilterActivityForResult(Activity, int)} or
+ * {@link #startAccountFilterActivityForResult(Fragment, int)}.
+ *
+ * This will update filter via a given ContactListFilterController.
+ */
+ public static void handleAccountFilterResult(
+ ContactListFilterController filterController, int resultCode, Intent data) {
+ if (resultCode == Activity.RESULT_OK) {
+ final ContactListFilter filter = (ContactListFilter)
+ data.getParcelableExtra(AccountFilterActivity.KEY_EXTRA_CONTACT_LIST_FILTER);
+ if (filter == null) {
+ return;
+ }
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+ filterController.selectCustomFilter();
+ } else {
+ filterController.setContactListFilter(filter, true);
+ }
+ }
+ }
+}
diff --git a/src/com/android/contacts/common/util/EmptyService.java b/src/com/android/contacts/common/util/EmptyService.java
new file mode 100644
index 00000000..c5c36080
--- /dev/null
+++ b/src/com/android/contacts/common/util/EmptyService.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2009 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.common.util;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Background {@link Service} that is used to keep our process alive long enough
+ * for background threads to finish. Started and stopped directly by specific
+ * background tasks when needed.
+ */
+public class EmptyService extends Service {
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+}
diff --git a/src/com/android/contacts/common/util/LocalizedNameResolver.java b/src/com/android/contacts/common/util/LocalizedNameResolver.java
new file mode 100644
index 00000000..f8d81511
--- /dev/null
+++ b/src/com/android/contacts/common/util/LocalizedNameResolver.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2010 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.common.util;
+
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * Retrieves localized names per account type. This allows customizing texts like
+ * "All Contacts" for certain account types, but e.g. "All Friends" or "All Connections" for others.
+ */
+public class LocalizedNameResolver {
+ private static final String TAG = "LocalizedNameResolver";
+
+ /**
+ * Meta-data key for the contacts configuration associated with a sync service.
+ */
+ private static final String METADATA_CONTACTS = "android.provider.CONTACTS_STRUCTURE";
+
+ private static final String CONTACTS_DATA_KIND = "ContactsDataKind";
+
+ /**
+ * Returns the name for All Contacts for the specified account type.
+ */
+ public static String getAllContactsName(Context context, String accountType) {
+ if (context == null) throw new IllegalArgumentException("Context must not be null");
+ if (accountType == null) return null;
+
+ return resolveAllContactsName(context, accountType);
+ }
+
+ /**
+ * Finds "All Contacts"-Name for the specified account type.
+ */
+ private static String resolveAllContactsName(Context context, String accountType) {
+ final AccountManager am = AccountManager.get(context);
+
+ for (AuthenticatorDescription auth : am.getAuthenticatorTypes()) {
+ if (accountType.equals(auth.type)) {
+ return resolveAllContactsNameFromMetaData(context, auth.packageName);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the meta-data XML containing the contacts configuration and
+ * reads the picture priority from that file.
+ */
+ private static String resolveAllContactsNameFromMetaData(Context context, String packageName) {
+ final PackageManager pm = context.getPackageManager();
+ try {
+ PackageInfo pi = pm.getPackageInfo(packageName, PackageManager.GET_SERVICES
+ | PackageManager.GET_META_DATA);
+ if (pi != null && pi.services != null) {
+ for (ServiceInfo si : pi.services) {
+ final XmlResourceParser parser = si.loadXmlMetaData(pm, METADATA_CONTACTS);
+ if (parser != null) {
+ return loadAllContactsNameFromXml(context, parser, packageName);
+ }
+ }
+ }
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Problem loading \"All Contacts\"-name: " + e.toString());
+ }
+ return null;
+ }
+
+ private static String loadAllContactsNameFromXml(Context context, XmlPullParser parser,
+ String packageName) {
+ try {
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Drain comments and whitespace
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("No start tag found");
+ }
+
+ final int depth = parser.getDepth();
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+ String name = parser.getName();
+ if (type == XmlPullParser.START_TAG && CONTACTS_DATA_KIND.equals(name)) {
+ final TypedArray typedArray = context.obtainStyledAttributes(attrs,
+ android.R.styleable.ContactsDataKind);
+ try {
+ // See if a string has been hardcoded directly into the xml
+ final String nonResourceString = typedArray.getNonResourceString(
+ android.R.styleable.ContactsDataKind_allContactsName);
+ if (nonResourceString != null) {
+ return nonResourceString;
+ }
+
+ // See if a resource is referenced. We can't rely on getString
+ // to automatically resolve it as the resource lives in a different package
+ int id = typedArray.getResourceId(
+ android.R.styleable.ContactsDataKind_allContactsName, 0);
+ if (id == 0) return null;
+
+ // Resolve the resource Id
+ final PackageManager packageManager = context.getPackageManager();
+ final Resources resources;
+ try {
+ resources = packageManager.getResourcesForApplication(packageName);
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ try {
+ return resources.getString(id);
+ } catch (NotFoundException e) {
+ return null;
+ }
+ } finally {
+ typedArray.recycle();
+ }
+ }
+ }
+ return null;
+ } catch (XmlPullParserException e) {
+ throw new IllegalStateException("Problem reading XML", e);
+ } catch (IOException e) {
+ throw new IllegalStateException("Problem reading XML", e);
+ }
+ }
+}
diff --git a/src/com/android/contacts/common/util/WeakAsyncTask.java b/src/com/android/contacts/common/util/WeakAsyncTask.java
new file mode 100644
index 00000000..f46e5142
--- /dev/null
+++ b/src/com/android/contacts/common/util/WeakAsyncTask.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2009 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.common.util;
+
+import android.os.AsyncTask;
+
+import java.lang.ref.WeakReference;
+
+public abstract class WeakAsyncTask<Params, Progress, Result, WeakTarget> extends
+ AsyncTask<Params, Progress, Result> {
+ protected WeakReference<WeakTarget> mTarget;
+
+ public WeakAsyncTask(WeakTarget target) {
+ mTarget = new WeakReference<WeakTarget>(target);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected final void onPreExecute() {
+ final WeakTarget target = mTarget.get();
+ if (target != null) {
+ this.onPreExecute(target);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected final Result doInBackground(Params... params) {
+ final WeakTarget target = mTarget.get();
+ if (target != null) {
+ return this.doInBackground(target, params);
+ } else {
+ return null;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected final void onPostExecute(Result result) {
+ final WeakTarget target = mTarget.get();
+ if (target != null) {
+ this.onPostExecute(target, result);
+ }
+ }
+
+ protected void onPreExecute(WeakTarget target) {
+ // No default action
+ }
+
+ protected abstract Result doInBackground(WeakTarget target, Params... params);
+
+ protected void onPostExecute(WeakTarget target, Result result) {
+ // No default action
+ }
+}