am 2585921d: Merge "Fixing search in direct dial/message shortcut creation" into froyo
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 04d0cf4..a7eec1d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -224,15 +224,20 @@
<data android:mimeType="vnd.android.cursor.item/postal-address" android:host="contacts" />
</intent-filter>
+ <intent-filter>
+ <action android:name="com.android.contacts.action.GET_MULTIPLE_PHONES" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="vnd.android.cursor.dir/phone_v2" android:host="com.android.contacts" />
+ </intent-filter>
</activity>
<!-- An activity for joining contacts -->
- <activity android:name="ContactsListActivity$JoinContactActivity"
+ <activity android:name="JoinContactActivity"
android:theme="@style/TallTitleBarTheme"
android:clearTaskOnLaunch="true"
>
<intent-filter>
- <action android:name="com.android.contacts.action.JOIN_AGGREGATE" />
+ <action android:name="com.android.contacts.action.JOIN_CONTACT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
@@ -347,7 +352,7 @@
</activity>
<!-- Views the details of a single contact -->
- <activity android:name="ViewContactActivity"
+ <activity android:name=".activities.ContactDetailActivity"
android:label="@string/viewContactTitle"
android:theme="@style/TallTitleBarTheme">
diff --git a/res/anim/footer_appear.xml b/res/anim/footer_appear.xml
new file mode 100644
index 0000000..941454a
--- /dev/null
+++ b/res/anim/footer_appear.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromYDelta="+10%p"
+ android:toYDelta="0"
+ android:duration="300" />
\ No newline at end of file
diff --git a/res/drawable-hdpi-finger/ic_menu_display_all.png b/res/drawable-hdpi-finger/ic_menu_display_all.png
new file mode 100755
index 0000000..563083c
--- /dev/null
+++ b/res/drawable-hdpi-finger/ic_menu_display_all.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_menu_display_selected.png b/res/drawable-hdpi-finger/ic_menu_display_selected.png
new file mode 100644
index 0000000..76b2e22
--- /dev/null
+++ b/res/drawable-hdpi-finger/ic_menu_display_selected.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_menu_select.png b/res/drawable-hdpi-finger/ic_menu_select.png
new file mode 100644
index 0000000..c5bb503
--- /dev/null
+++ b/res/drawable-hdpi-finger/ic_menu_select.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_menu_unselect.png b/res/drawable-hdpi-finger/ic_menu_unselect.png
new file mode 100644
index 0000000..178f314
--- /dev/null
+++ b/res/drawable-hdpi-finger/ic_menu_unselect.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_1.9.png b/res/drawable-hdpi/appointment_indicator_leftside_1.9.png
new file mode 100644
index 0000000..b72652b
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_1.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_10.9.png b/res/drawable-hdpi/appointment_indicator_leftside_10.9.png
new file mode 100644
index 0000000..ff09049
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_10.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_11.9.png b/res/drawable-hdpi/appointment_indicator_leftside_11.9.png
new file mode 100644
index 0000000..6a2e4f2
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_11.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_12.9.png b/res/drawable-hdpi/appointment_indicator_leftside_12.9.png
new file mode 100644
index 0000000..0f19c83
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_12.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_13.9.png b/res/drawable-hdpi/appointment_indicator_leftside_13.9.png
new file mode 100644
index 0000000..7501e35
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_13.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_14.9.png b/res/drawable-hdpi/appointment_indicator_leftside_14.9.png
new file mode 100644
index 0000000..53f97a6
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_14.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_15.9.png b/res/drawable-hdpi/appointment_indicator_leftside_15.9.png
new file mode 100644
index 0000000..846f6f8
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_15.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_16.9.png b/res/drawable-hdpi/appointment_indicator_leftside_16.9.png
new file mode 100644
index 0000000..1707540
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_16.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_17.9.png b/res/drawable-hdpi/appointment_indicator_leftside_17.9.png
new file mode 100644
index 0000000..7fd945d
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_17.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_18.9.png b/res/drawable-hdpi/appointment_indicator_leftside_18.9.png
new file mode 100644
index 0000000..8cf47ae
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_18.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_19.9.png b/res/drawable-hdpi/appointment_indicator_leftside_19.9.png
new file mode 100644
index 0000000..6831c01
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_19.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_2.9.png b/res/drawable-hdpi/appointment_indicator_leftside_2.9.png
new file mode 100644
index 0000000..b4cee11
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_2.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_20.9.png b/res/drawable-hdpi/appointment_indicator_leftside_20.9.png
new file mode 100644
index 0000000..d07d826
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_20.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_21.9.png b/res/drawable-hdpi/appointment_indicator_leftside_21.9.png
new file mode 100644
index 0000000..f410269
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_21.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_3.9.png b/res/drawable-hdpi/appointment_indicator_leftside_3.9.png
new file mode 100644
index 0000000..69bd6a9
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_3.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_4.9.png b/res/drawable-hdpi/appointment_indicator_leftside_4.9.png
new file mode 100644
index 0000000..d09ea90
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_4.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_5.9.png b/res/drawable-hdpi/appointment_indicator_leftside_5.9.png
new file mode 100644
index 0000000..d27fc91
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_5.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_6.9.png b/res/drawable-hdpi/appointment_indicator_leftside_6.9.png
new file mode 100644
index 0000000..c014633
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_6.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_7.9.png b/res/drawable-hdpi/appointment_indicator_leftside_7.9.png
new file mode 100644
index 0000000..febb514
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_7.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_8.9.png b/res/drawable-hdpi/appointment_indicator_leftside_8.9.png
new file mode 100644
index 0000000..1415e44
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_8.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_9.9.png b/res/drawable-hdpi/appointment_indicator_leftside_9.9.png
new file mode 100644
index 0000000..d018fcf
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_9.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_display_all.png b/res/drawable-mdpi-finger/ic_menu_display_all.png
new file mode 100644
index 0000000..61a9e35
--- /dev/null
+++ b/res/drawable-mdpi-finger/ic_menu_display_all.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_display_selected.png b/res/drawable-mdpi-finger/ic_menu_display_selected.png
new file mode 100644
index 0000000..b4ec7a8
--- /dev/null
+++ b/res/drawable-mdpi-finger/ic_menu_display_selected.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_select.png b/res/drawable-mdpi-finger/ic_menu_select.png
new file mode 100644
index 0000000..29e3d7e
--- /dev/null
+++ b/res/drawable-mdpi-finger/ic_menu_select.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_unselect.png b/res/drawable-mdpi-finger/ic_menu_unselect.png
new file mode 100644
index 0000000..2b69bc8
--- /dev/null
+++ b/res/drawable-mdpi-finger/ic_menu_unselect.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_1.9.png b/res/drawable-mdpi/appointment_indicator_leftside_1.9.png
new file mode 100644
index 0000000..5e40235
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_1.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_10.9.png b/res/drawable-mdpi/appointment_indicator_leftside_10.9.png
new file mode 100644
index 0000000..d0cb144
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_10.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_11.9.png b/res/drawable-mdpi/appointment_indicator_leftside_11.9.png
new file mode 100644
index 0000000..034f496
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_11.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_12.9.png b/res/drawable-mdpi/appointment_indicator_leftside_12.9.png
new file mode 100644
index 0000000..6371b3a
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_12.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_13.9.png b/res/drawable-mdpi/appointment_indicator_leftside_13.9.png
new file mode 100644
index 0000000..a8b42c6
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_13.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_14.9.png b/res/drawable-mdpi/appointment_indicator_leftside_14.9.png
new file mode 100644
index 0000000..a69e519
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_14.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_15.9.png b/res/drawable-mdpi/appointment_indicator_leftside_15.9.png
new file mode 100644
index 0000000..5d68470
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_15.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_16.9.png b/res/drawable-mdpi/appointment_indicator_leftside_16.9.png
new file mode 100644
index 0000000..d9420c1
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_16.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_17.9.png b/res/drawable-mdpi/appointment_indicator_leftside_17.9.png
new file mode 100644
index 0000000..d0875c4
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_17.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_18.9.png b/res/drawable-mdpi/appointment_indicator_leftside_18.9.png
new file mode 100644
index 0000000..fc152f7
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_18.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_19.9.png b/res/drawable-mdpi/appointment_indicator_leftside_19.9.png
new file mode 100644
index 0000000..6506a94
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_19.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_2.9.png b/res/drawable-mdpi/appointment_indicator_leftside_2.9.png
new file mode 100644
index 0000000..3baf5cc
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_2.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_20.9.png b/res/drawable-mdpi/appointment_indicator_leftside_20.9.png
new file mode 100644
index 0000000..28340ba
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_20.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_21.9.png b/res/drawable-mdpi/appointment_indicator_leftside_21.9.png
new file mode 100644
index 0000000..5319f07
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_21.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_3.9.png b/res/drawable-mdpi/appointment_indicator_leftside_3.9.png
new file mode 100644
index 0000000..9850791
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_3.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_4.9.png b/res/drawable-mdpi/appointment_indicator_leftside_4.9.png
new file mode 100644
index 0000000..e344ccb
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_4.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_5.9.png b/res/drawable-mdpi/appointment_indicator_leftside_5.9.png
new file mode 100644
index 0000000..11b4dfb
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_5.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_6.9.png b/res/drawable-mdpi/appointment_indicator_leftside_6.9.png
new file mode 100644
index 0000000..7419d47
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_6.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_7.9.png b/res/drawable-mdpi/appointment_indicator_leftside_7.9.png
new file mode 100644
index 0000000..0a3a272
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_7.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_8.9.png b/res/drawable-mdpi/appointment_indicator_leftside_8.9.png
new file mode 100644
index 0000000..db18d27
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_8.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_9.9.png b/res/drawable-mdpi/appointment_indicator_leftside_9.9.png
new file mode 100644
index 0000000..5037de8
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_9.9.png
Binary files differ
diff --git a/res/layout-finger/contacts_list_content.xml b/res/layout-finger/contacts_list_content.xml
index 36c03ce..4dd680f 100644
--- a/res/layout-finger/contacts_list_content.xml
+++ b/res/layout-finger/contacts_list_content.xml
@@ -26,10 +26,16 @@
class="com.android.contacts.PinnedHeaderListView"
android:id="@android:id/list"
android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_height="0dip"
android:fastScrollEnabled="true"
+ android:layout_weight="1"
/>
<include layout="@layout/contacts_list_empty"/>
+ <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-finger/contacts_list_empty.xml b/res/layout-finger/contacts_list_empty.xml
index 195da1e..d655899 100644
--- a/res/layout-finger/contacts_list_empty.xml
+++ b/res/layout-finger/contacts_list_empty.xml
@@ -13,8 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<ScrollView
+<view
xmlns:android="http://schemas.android.com/apk/res/android"
+ class="com.android.contacts.ContactListEmptyView"
android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -35,6 +36,7 @@
android:paddingRight="10dip"
android:paddingTop="10dip"
android:lineSpacingMultiplier="0.92"
+ android:visibility="gone"
/>
<LinearLayout android:id="@+id/import_failure"
@@ -61,4 +63,4 @@
android:text="@string/upgrade_out_of_memory_retry"/>
</LinearLayout>
</LinearLayout>
-</ScrollView>
+</view>
diff --git a/res/layout-finger/contacts_search_content.xml b/res/layout-finger/contacts_search_content.xml
index ae72376..d9479dc 100644
--- a/res/layout-finger/contacts_search_content.xml
+++ b/res/layout-finger/contacts_search_content.xml
@@ -29,7 +29,8 @@
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
android:fastScrollEnabled="true"
android:background="@android:color/background_dark"
/>
@@ -37,6 +38,12 @@
<!-- Transparent filler -->
<View android:id="@android:id/empty"
android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ />
+ <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-finger/footer_panel.xml b/res/layout-finger/footer_panel.xml
new file mode 100644
index 0000000..2625a43
--- /dev/null
+++ b/res/layout-finger/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/layout/contact_detail.xml b/res/layout/contact_detail.xml
new file mode 100644
index 0000000..541ba1c
--- /dev/null
+++ b/res/layout/contact_detail.xml
@@ -0,0 +1,53 @@
+<?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.
+-->
+
+<com.android.contacts.views.detail.ContactDetailView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/contact_details"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.internal.widget.ContactHeaderWidget
+ android:id="@+id/contact_header_widget"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <ListView android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/title_bar_shadow"
+ />
+
+ <ScrollView android:id="@android:id/empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="true"
+ >
+ <TextView android:id="@+id/emptyText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/no_contact_details"
+ android:textSize="20sp"
+ android:textColor="?android:attr/textColorSecondary"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ android:paddingTop="10dip"
+ android:lineSpacingMultiplier="0.92"
+ />
+ </ScrollView>
+
+</com.android.contacts.views.detail.ContactDetailView>
+
diff --git a/res/menu/pick.xml b/res/menu/pick.xml
new file mode 100644
index 0000000..5302dd9
--- /dev/null
+++ b/res/menu/pick.xml
@@ -0,0 +1,39 @@
+<?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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/menu_display_selected"
+ android:icon="@drawable/ic_menu_display_selected"
+ android:title="@string/menu_display_selected" />
+
+ <item
+ android:id="@+id/menu_display_all"
+ android:icon="@drawable/ic_menu_display_all"
+ android:title="@string/menu_display_all" />
+
+ <item
+ android:id="@+id/menu_select_all"
+ android:icon="@drawable/ic_menu_select"
+ android:title="@string/menu_select_all" />
+
+ <item
+ android:id="@+id/menu_select_none"
+ android:icon="@drawable/ic_menu_unselect"
+ android:title="@string/menu_select_none" />
+
+</menu>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 4a7a743..3bc7ff6 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -39,4 +39,8 @@
<dimen name="list_item_vertical_divider_margin">5dip</dimen>
<dimen name="list_item_presence_icon_margin">5dip</dimen>
<dimen name="list_item_header_text_width">56dip</dimen>
+ <dimen name="list_item_header_chip_width">4dip</dimen>
+ <dimen name="list_item_header_chip_right_margin">4dip</dimen>
+ <dimen name="list_item_header_checkbox_margin">5dip</dimen>
+
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 041440a..abdf496 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1134,4 +1134,34 @@
<!-- Title shown in the search result activity of contacts app while searching -->
<string name="search_results_searching">Searching...</string>
+
+ <!-- Message of progress dialog for multiple picker -->
+ <string name="adding_recipients">"Loading \u2026"</string>
+
+ <!-- Label to display only selection in multiple picker -->
+ <string name="menu_display_selected">"Show selected"</string>
+
+ <!-- Label to display all recipients in multiple picker -->
+ <string name="menu_display_all">"Show all"</string>
+
+ <!-- Label to select all contacts in multiple picker -->
+ <string name="menu_select_all">"Select all"</string>
+
+ <!-- Label to clear all selection in multiple picker -->
+ <string name="menu_select_none">"Unselect all"</string>
+
+ <!-- Label to display how many selected in multiple picker -->
+ <plurals name="multiple_picker_title">
+ <!-- number of selected recipients is one -->
+ <item quantity="one">"1 recipient selected"</item>
+ <!-- number of selected recipients is not equal to one -->
+ <item quantity="other"><xliff:g id="count">%d</xliff:g>" recipients selected"</item>
+ </plurals>
+
+ <!-- Separator label to display unknown recipients in multiple picker -->
+ <string name="unknown_contacts_separator">"Unknown contacts"</string>
+
+ <!-- The text displayed when the contacts list is empty while displaying only selected contacts in multiple picker -->
+ <string name="no_contacts_selected">"No contacts selected."</string>
+
</resources>
diff --git a/src/com/android/contacts/ContactEntryAdapter.java b/src/com/android/contacts/ContactEntryAdapter.java
index 34ee505..90a41ca 100644
--- a/src/com/android/contacts/ContactEntryAdapter.java
+++ b/src/com/android/contacts/ContactEntryAdapter.java
@@ -77,7 +77,8 @@
}
}
- ContactEntryAdapter(Context context, ArrayList<ArrayList<E>> sections, boolean separators) {
+ protected ContactEntryAdapter(Context context, ArrayList<ArrayList<E>> sections,
+ boolean separators) {
mContext = context;
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mSections = sections;
diff --git a/src/com/android/contacts/ContactListEmptyView.java b/src/com/android/contacts/ContactListEmptyView.java
new file mode 100644
index 0000000..58573f1
--- /dev/null
+++ b/src/com/android/contacts/ContactListEmptyView.java
@@ -0,0 +1,114 @@
+/*
+ * 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;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.IContentService;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.telephony.TelephonyManager;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+/**
+ * Displays a message when there is nothing to display in a contact list.
+ */
+public class ContactListEmptyView extends ScrollView {
+
+ private static final String TAG = "ContactListEmptyView";
+
+ public ContactListEmptyView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void hide() {
+ TextView empty = (TextView) findViewById(R.id.emptyText);
+ empty.setVisibility(GONE);
+ }
+
+ protected void show(boolean searchMode, boolean displayOnlyPhones,
+ boolean isFavoritesMode, boolean isQueryMode, boolean isShortcutAction,
+ boolean isMultipleSelectionEnabled, boolean showSelectedOnly) {
+ if (searchMode) {
+ return;
+ }
+
+ TextView empty = (TextView) findViewById(R.id.emptyText);
+ Context context = getContext();
+ if (displayOnlyPhones) {
+ empty.setText(context.getText(R.string.noContactsWithPhoneNumbers));
+ } else if (isFavoritesMode) {
+ empty.setText(context.getText(R.string.noFavoritesHelpText));
+ } else if (isQueryMode) {
+ empty.setText(context.getText(R.string.noMatchingContacts));
+ } if (isMultipleSelectionEnabled) {
+ if (showSelectedOnly) {
+ empty.setText(context.getText(R.string.no_contacts_selected));
+ } else {
+ empty.setText(context.getText(R.string.noContactsWithPhoneNumbers));
+ }
+ } else {
+ TelephonyManager telephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ boolean hasSim = telephonyManager.hasIccCard();
+ if (isSyncActive()) {
+ if (isShortcutAction) {
+ // Help text is the same no matter whether there is SIM or not.
+ empty.setText(
+ context.getText(R.string.noContactsHelpTextWithSyncForCreateShortcut));
+ } else if (hasSim) {
+ empty.setText(context.getText(R.string.noContactsHelpTextWithSync));
+ } else {
+ empty.setText(context.getText(R.string.noContactsNoSimHelpTextWithSync));
+ }
+ } else {
+ if (isShortcutAction) {
+ // Help text is the same no matter whether there is SIM or not.
+ empty.setText(context.getText(R.string.noContactsHelpTextForCreateShortcut));
+ } else if (hasSim) {
+ empty.setText(context.getText(R.string.noContactsHelpText));
+ } else {
+ empty.setText(context.getText(R.string.noContactsNoSimHelpText));
+ }
+ }
+ }
+ empty.setVisibility(VISIBLE);
+ }
+
+ private boolean isSyncActive() {
+ Account[] accounts = AccountManager.get(getContext()).getAccounts();
+ if (accounts != null && accounts.length > 0) {
+ IContentService contentService = ContentResolver.getContentService();
+ for (Account account : accounts) {
+ try {
+ if (contentService.isSyncActive(account, ContactsContract.AUTHORITY)) {
+ return true;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not get the sync status");
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/contacts/ContactListItemView.java b/src/com/android/contacts/ContactListItemView.java
index 89e4265..db2bb48 100644
--- a/src/com/android/contacts/ContactListItemView.java
+++ b/src/com/android/contacts/ContactListItemView.java
@@ -31,6 +31,7 @@
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.QuickContactBadge;
import android.widget.TextView;
@@ -79,14 +80,22 @@
private TextView mDataView;
private TextView mSnippetView;
private ImageView mPresenceIcon;
+ // Used to indicate the sequence of phones belong to the same contact in multi-picker
+ private View mChipView;
+ // Used to select the phone in multi-picker
+ private CheckBox mCheckBox;
private int mPhotoViewWidth;
private int mPhotoViewHeight;
private int mLine1Height;
private int mLine2Height;
private int mLine3Height;
+ private int mChipWidth;
+ private int mChipRightMargin;
+ private int mCheckBoxMargin;
private OnClickListener mCallButtonClickListener;
+ private OnClickListener mCheckBoxClickListener;
public ContactListItemView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -119,6 +128,12 @@
resources.getDimensionPixelOffset(R.dimen.list_item_presence_icon_margin);
mHeaderTextWidth =
resources.getDimensionPixelOffset(R.dimen.list_item_header_text_width);
+ mChipWidth =
+ resources.getDimensionPixelOffset(R.dimen.list_item_header_chip_width);
+ mChipRightMargin =
+ resources.getDimensionPixelOffset(R.dimen.list_item_header_chip_right_margin);
+ mCheckBoxMargin =
+ resources.getDimensionPixelOffset(R.dimen.list_item_header_checkbox_margin);
}
/**
@@ -128,6 +143,9 @@
mCallButtonClickListener = callButtonClickListener;
}
+ public void setOnCheckBoxClickListener(OnClickListener checkBoxClickListener) {
+ mCheckBoxClickListener = checkBoxClickListener;
+ }
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// We will match parent's width and wrap content vertically, but make sure
@@ -168,6 +186,14 @@
mPresenceIcon.measure(0, 0);
}
+ if (isVisible(mChipView)) {
+ mChipView.measure(0, 0);
+ }
+
+ if (isVisible(mCheckBox)) {
+ mCheckBox.measure(0, 0);
+ }
+
ensurePhotoViewSize();
height = Math.max(height, mPhotoViewHeight);
@@ -209,6 +235,10 @@
// Left side
int leftBound = mPaddingLeft;
+ if (mChipView != null) {
+ mChipView.layout(leftBound, topBound, leftBound + mChipWidth, height);
+ leftBound += mChipWidth + mChipRightMargin;
+ }
View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
if (photoView != null) {
// Center the photo vertically
@@ -252,7 +282,17 @@
rightBound + iconWidth,
height);
}
-
+ if (isVisible(mCheckBox)) {
+ int checkBoxWidth = mCheckBox.getMeasuredWidth();
+ int checkBoxHight = mCheckBox.getMeasuredHeight();
+ rightBound -= mCheckBoxMargin + checkBoxWidth;
+ int checkBoxTop = topBound + (height - topBound - checkBoxHight) / 2;
+ mCheckBox.layout(
+ rightBound,
+ checkBoxTop,
+ rightBound + checkBoxWidth,
+ checkBoxTop + checkBoxHight);
+ }
if (mHorizontalDividerVisible) {
ensureHorizontalDivider();
mHorizontalDividerDrawable.setBounds(
@@ -575,6 +615,29 @@
}
/**
+ * Returns the chip view for the multipicker, creating it if necessary.
+ */
+ public View getChipView() {
+ if (mChipView == null) {
+ mChipView = new View(mContext);
+ addView(mChipView);
+ }
+ return mChipView;
+ }
+
+ /**
+ * Returns the CheckBox view for the multipicker, creating it if necessary.
+ */
+ public CheckBox getCheckBoxView() {
+ if (mCheckBox == null) {
+ mCheckBox = new CheckBox(mContext);
+ mCheckBox.setOnClickListener(mCheckBoxClickListener);
+ addView(mCheckBox);
+ }
+ return mCheckBox;
+ }
+
+ /**
* Adds or updates the presence icon view.
*/
public void setPresence(Drawable icon) {
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 4169e37..22cb78f 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -26,11 +26,11 @@
import com.android.contacts.util.Constants;
import android.accounts.Account;
-import android.accounts.AccountManager;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ListActivity;
+import android.app.ProgressDialog;
import android.app.SearchManager;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
@@ -38,7 +38,6 @@
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
-import android.content.IContentService;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.UriMatcher;
@@ -58,11 +57,9 @@
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
-import android.net.Uri.Builder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
-import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.provider.Settings;
@@ -83,7 +80,6 @@
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.Contacts.AggregationSuggestions;
import android.provider.ContactsContract.Intents.Insert;
import android.provider.ContactsContract.Intents.UI;
import android.telephony.TelephonyManager;
@@ -92,6 +88,7 @@
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
+import android.util.SparseIntArray;
import android.view.ContextMenu;
import android.view.ContextThemeWrapper;
import android.view.KeyEvent;
@@ -102,16 +99,20 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewStub;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.View.OnTouchListener;
+import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
import android.widget.Button;
+import android.widget.CheckBox;
import android.widget.CursorAdapter;
import android.widget.Filter;
import android.widget.ImageView;
@@ -124,6 +125,8 @@
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Random;
@@ -135,10 +138,6 @@
View.OnClickListener, View.OnKeyListener, TextWatcher, TextView.OnEditorActionListener,
OnFocusChangeListener, OnTouchListener {
- public static class JoinContactActivity extends ContactsListActivity {
-
- }
-
public static class ContactsSearchActivity extends ContactsListActivity {
}
@@ -167,33 +166,6 @@
private static final int TEXT_HIGHLIGHTING_ANIMATION_DURATION = 350;
- /**
- * The action for the join contact activity.
- * <p>
- * Input: extra field {@link #EXTRA_AGGREGATE_ID} is the aggregate ID.
- *
- * TODO: move to {@link ContactsContract}.
- */
- public static final String JOIN_AGGREGATE =
- "com.android.contacts.action.JOIN_AGGREGATE";
-
- /**
- * Used with {@link #JOIN_AGGREGATE} to give it the target for aggregation.
- * <p>
- * Type: LONG
- */
- public static final String EXTRA_AGGREGATE_ID =
- "com.android.contacts.action.AGGREGATE_ID";
-
- /**
- * Used with {@link #JOIN_AGGREGATE} to give it the name of the aggregation target.
- * <p>
- * Type: STRING
- */
- @Deprecated
- public static final String EXTRA_AGGREGATE_NAME =
- "com.android.contacts.action.AGGREGATE_NAME";
-
public static final String AUTHORITIES_FILTER_KEY = "authorities";
private static final Uri CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS =
@@ -264,10 +236,6 @@
static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_PICKER
| MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
- /** Show join suggestions followed by an A-Z list */
- static final int MODE_JOIN_CONTACT = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE
- | MODE_MASK_NO_DATA | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
-
/** Run a search query in a PICK mode */
static final int MODE_QUERY_PICK = 75 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_NO_FILTER
| MODE_MASK_PICKER | MODE_MASK_DISABLE_QUIKCCONTACT | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
@@ -281,13 +249,17 @@
| MODE_MASK_PICKER | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
/**
+ * Show all phone numbers and do multiple pick when clicking. This mode has phone filtering
+ * feature, but doesn't support 'search for all contacts'.
+ */
+ static final int MODE_PICK_MULTIPLE_PHONES = 80 | MODE_MASK_PICKER
+ | MODE_MASK_NO_PRESENCE | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
+
+ /**
* An action used to do perform search while in a contact picker. It is initiated
* by the ContactListActivity itself.
*/
- private static final String ACTION_SEARCH_INTERNAL = "com.android.contacts.INTERNAL_SEARCH";
-
- /** Maximum number of suggestions shown for joining aggregates */
- static final int MAX_SUGGESTIONS = 4;
+ protected static final String ACTION_SEARCH_INTERNAL = "com.android.contacts.INTERNAL_SEARCH";
static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
Contacts._ID, // 0
@@ -364,6 +336,8 @@
Phone.NUMBER, //3
Phone.DISPLAY_NAME, // 4
Phone.CONTACT_ID, // 5
+ Contacts.SORT_KEY_PRIMARY, // 6
+ Contacts.PHOTO_ID, // 7
};
static final String[] LEGACY_PHONES_PROJECTION = new String[] {
Phones._ID, //0
@@ -378,6 +352,8 @@
static final int PHONE_NUMBER_COLUMN_INDEX = 3;
static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 4;
static final int PHONE_CONTACT_ID_COLUMN_INDEX = 5;
+ static final int PHONE_SORT_KEY_PRIMARY_COLUMN_INDEX = 6;
+ static final int PHONE_PHOTO_ID_COLUMN_INDEX = 7;
static final String[] POSTALS_PROJECTION = new String[] {
StructuredPostal._ID, //0
@@ -409,7 +385,11 @@
static final String KEY_PICKER_MODE = "picker_mode";
+ private static final String TEL_SCHEME = "tel";
+ private static final String CONTENT_SCHEME = "content";
+
private ContactItemListAdapter mAdapter;
+ private ContactListEmptyView mEmptyView;
int mMode = MODE_DEFAULT;
@@ -423,8 +403,6 @@
private Uri mGroupUri;
- private long mQueryAggregateId;
-
private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
private int mWritableSourcesCnt;
private int mReadOnlySourcesCnt;
@@ -459,16 +437,6 @@
private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
private static final String CLAUSE_ONLY_PHONES = Contacts.HAS_PHONE_NUMBER + "=1";
- /**
- * In the {@link #MODE_JOIN_CONTACT} determines whether we display a list item with the label
- * "Show all contacts" or actually show all contacts
- */
- private boolean mJoinModeShowAllContacts;
-
- /**
- * The ID of the special item described above.
- */
- private static final long JOIN_MODE_SHOW_ALL_CONTACTS_ID = -2;
// Uri matcher for contact id
private static final int CONTACTS_ID = 1001;
@@ -480,6 +448,57 @@
Contacts.LOOKUP_KEY
};
+ /**
+ * User selected phone number and id in MODE_PICK_MULTIPLE_PHONES mode.
+ */
+ private UserSelection mUserSelection = new UserSelection(null, null);
+
+ /**
+ * The adapter for the phone numbers, used in MODE_PICK_MULTIPLE_PHONES mode.
+ */
+ private PhoneNumberAdapter mPhoneNumberAdapter = new PhoneNumberAdapter(this, null);
+
+ private static int[] CHIP_COLOR_ARRAY = {
+ R.drawable.appointment_indicator_leftside_1,
+ R.drawable.appointment_indicator_leftside_2,
+ R.drawable.appointment_indicator_leftside_3,
+ R.drawable.appointment_indicator_leftside_4,
+ R.drawable.appointment_indicator_leftside_5,
+ R.drawable.appointment_indicator_leftside_6,
+ R.drawable.appointment_indicator_leftside_7,
+ R.drawable.appointment_indicator_leftside_8,
+ R.drawable.appointment_indicator_leftside_9,
+ R.drawable.appointment_indicator_leftside_10,
+ R.drawable.appointment_indicator_leftside_11,
+ R.drawable.appointment_indicator_leftside_12,
+ R.drawable.appointment_indicator_leftside_13,
+ R.drawable.appointment_indicator_leftside_14,
+ R.drawable.appointment_indicator_leftside_15,
+ R.drawable.appointment_indicator_leftside_16,
+ R.drawable.appointment_indicator_leftside_17,
+ R.drawable.appointment_indicator_leftside_18,
+ R.drawable.appointment_indicator_leftside_19,
+ R.drawable.appointment_indicator_leftside_20,
+ R.drawable.appointment_indicator_leftside_21,
+ };
+
+ /**
+ * This is the map from contact to color index.
+ * A colored chip in MODE_PICK_MULTIPLE_PHONES mode is used to indicate the number of phone
+ * numbers belong to one contact
+ */
+ SparseIntArray mContactColor;
+
+ /**
+ * UI control of action panel in MODE_PICK_MULTIPLE_PHONES mode.
+ */
+ private View mFooterView;
+
+ /**
+ * Display only selected recipients or not in MODE_PICK_MULTIPLE_PHONES mode
+ */
+ private boolean mShowSelectedOnly = false;
+
static {
sContactsIdMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
@@ -557,6 +576,19 @@
}
};
+ private OnClickListener mCheckBoxClickerListener = new OnClickListener () {
+ public void onClick(View v) {
+ final ContactListItemCache cache = (ContactListItemCache) v.getTag();
+ if (cache.phoneId != PhoneNumberAdapter.INVALID_PHONE_ID) {
+ mUserSelection.setPhoneSelected(cache.phoneId, ((CheckBox) v).isChecked());
+ } else {
+ mUserSelection.setPhoneSelected(cache.phoneNumber,
+ ((CheckBox) v).isChecked());
+ }
+ updateWidgets(true);
+ }
+ };
+
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
@@ -565,9 +597,18 @@
mContactsPrefs = new ContactsPreferences(this);
mPhotoLoader = new ContactPhotoLoader(this, R.drawable.ic_contact_list_picture);
+ mQueryHandler = new QueryHandler(this);
+ mJustCreated = true;
+ mSyncEnabled = true;
+
// Resolve the intent
final Intent intent = getIntent();
+ resolveIntent(intent);
+ initContentView();
+ }
+
+ protected void resolveIntent(final Intent intent) {
// Allow the title to be set to a custom String using an extra on the intent
String title = intent.getStringExtra(UI.TITLE_EXTRA_KEY);
if (title != null) {
@@ -576,6 +617,7 @@
String action = intent.getAction();
String component = intent.getComponent().getClassName();
+ String type = intent.getType();
// When we get a FILTER_CONTACTS_ACTION, it represents search in the context
// of some other action. Let's retrieve the original action to provide proper
@@ -596,6 +638,11 @@
if (originalComponent != null) {
component = originalComponent;
}
+ String originalType =
+ extras.getString(ContactsSearchManager.ORIGINAL_TYPE_EXTRA_KEY);
+ if (originalType != null) {
+ type = originalType;
+ }
} else {
mInitialFilter = null;
}
@@ -630,18 +677,19 @@
} else if (Intent.ACTION_PICK.equals(action)) {
// XXX These should be showing the data from the URI given in
// the Intent.
- final String type = intent.resolveType(this);
- if (Contacts.CONTENT_TYPE.equals(type)) {
+ // TODO : Does it work in mSearchMode?
+ final String resolvedType = intent.resolveType(this);
+ if (Contacts.CONTENT_TYPE.equals(resolvedType)) {
mMode = MODE_PICK_CONTACT;
- } else if (People.CONTENT_TYPE.equals(type)) {
+ } else if (People.CONTENT_TYPE.equals(resolvedType)) {
mMode = MODE_LEGACY_PICK_PERSON;
- } else if (Phone.CONTENT_TYPE.equals(type)) {
+ } else if (Phone.CONTENT_TYPE.equals(resolvedType)) {
mMode = MODE_PICK_PHONE;
- } else if (Phones.CONTENT_TYPE.equals(type)) {
+ } else if (Phones.CONTENT_TYPE.equals(resolvedType)) {
mMode = MODE_LEGACY_PICK_PHONE;
- } else if (StructuredPostal.CONTENT_TYPE.equals(type)) {
+ } else if (StructuredPostal.CONTENT_TYPE.equals(resolvedType)) {
mMode = MODE_PICK_POSTAL;
- } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) {
+ } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(resolvedType)) {
mMode = MODE_LEGACY_PICK_POSTAL;
}
} else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
@@ -665,22 +713,23 @@
setTitle(R.string.shortcutActivityTitle);
}
} else if (Intent.ACTION_GET_CONTENT.equals(action)) {
- final String type = intent.resolveType(this);
- if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
+ // TODO : Does it work in mSearchMode?
+ final String resolvedType = intent.resolveType(this);
+ if (Contacts.CONTENT_ITEM_TYPE.equals(resolvedType)) {
if (mSearchMode) {
mMode = MODE_PICK_CONTACT;
} else {
mMode = MODE_PICK_OR_CREATE_CONTACT;
}
- } else if (Phone.CONTENT_ITEM_TYPE.equals(type)) {
+ } else if (Phone.CONTENT_ITEM_TYPE.equals(resolvedType)) {
mMode = MODE_PICK_PHONE;
- } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
+ } else if (Phones.CONTENT_ITEM_TYPE.equals(resolvedType)) {
mMode = MODE_LEGACY_PICK_PHONE;
- } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(type)) {
+ } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(resolvedType)) {
mMode = MODE_PICK_POSTAL;
- } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
+ } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(resolvedType)) {
mMode = MODE_LEGACY_PICK_POSTAL;
- } else if (People.CONTENT_ITEM_TYPE.equals(type)) {
+ } else if (People.CONTENT_ITEM_TYPE.equals(resolvedType)) {
if (mSearchMode) {
mMode = MODE_LEGACY_PICK_PERSON;
} else {
@@ -783,23 +832,23 @@
startActivity(newIntent);
finish();
return;
- }
-
- if (JOIN_AGGREGATE.equals(action)) {
+ } else if (JoinContactActivity.JOIN_CONTACT.equals(action)) {
+ mMode = MODE_PICK_CONTACT;
+ } else if (Intents.ACTION_GET_MULTIPLE_PHONES.equals(action)) {
if (mSearchMode) {
- mMode = MODE_PICK_CONTACT;
+ mShowSearchSnippets = false;
+ }
+ if (Phone.CONTENT_TYPE.equals(type)) {
+ mMode = MODE_PICK_MULTIPLE_PHONES;
+ mContactColor = new SparseIntArray();
+ initMultiPicker(intent);
} else {
- mMode = MODE_JOIN_CONTACT;
- mQueryAggregateId = intent.getLongExtra(EXTRA_AGGREGATE_ID, -1);
- if (mQueryAggregateId == -1) {
- Log.e(TAG, "Intent " + action + " is missing required extra: "
- + EXTRA_AGGREGATE_ID);
- setResult(RESULT_CANCELED);
- finish();
- }
+ // TODO support other content types
+ Log.e(TAG, "Intent " + action + " is not supported for type " + type);
+ setResult(RESULT_CANCELED);
+ finish();
}
}
-
if (mMode == MODE_UNKNOWN) {
mMode = MODE_DEFAULT;
}
@@ -808,16 +857,10 @@
&& !mSearchResultsMode) {
mShowNumberOfContacts = true;
}
+ }
- if (mMode == MODE_JOIN_CONTACT) {
- setContentView(R.layout.contacts_list_content_join);
- TextView blurbView = (TextView)findViewById(R.id.join_contact_blurb);
-
- String blurb = getString(R.string.blurbJoinContactDataWith,
- getContactDisplayName(mQueryAggregateId));
- blurbView.setText(blurb);
- mJoinModeShowAllContacts = true;
- } else if (mSearchMode) {
+ public void initContentView() {
+ if (mSearchMode) {
setContentView(R.layout.contacts_search_content);
} else if (mSearchResultsMode) {
setContentView(R.layout.contacts_list_search_results);
@@ -828,15 +871,28 @@
setContentView(R.layout.contacts_list_content);
}
- setupListView();
+ setupListView(new ContactItemListAdapter(this));
if (mSearchMode) {
setupSearchView();
}
- mQueryHandler = new QueryHandler(this);
- mJustCreated = true;
+ if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+ ViewStub stub = (ViewStub)findViewById(R.id.footer_stub);
+ if (stub != null) {
+ View stubView = stub.inflate();
+ mFooterView = stubView.findViewById(R.id.footer);
+ mFooterView.setVisibility(View.GONE);
+ Button doneButton = (Button) stubView.findViewById(R.id.done);
+ doneButton.setOnClickListener(this);
+ Button revertButton = (Button) stubView.findViewById(R.id.revert);
+ revertButton.setOnClickListener(this);
+ }
+ }
- mSyncEnabled = true;
+ View emptyView = mList.getEmptyView();
+ if (emptyView instanceof ContactListEmptyView) {
+ mEmptyView = (ContactListEmptyView)emptyView;
+ }
}
/**
@@ -856,7 +912,7 @@
getContentResolver().unregisterContentObserver(mProviderStatusObserver);
}
- private void setupListView() {
+ protected void setupListView(ContactItemListAdapter adapter) {
final ListView list = getListView();
final LayoutInflater inflater = getLayoutInflater();
@@ -868,7 +924,7 @@
list.setDividerHeight(0);
list.setOnCreateContextMenuListener(this);
- mAdapter = new ContactItemListAdapter(this);
+ mAdapter = adapter;
setListAdapter(mAdapter);
if (list instanceof PinnedHeaderListView && mAdapter.getDisplaySectionHeadersEnabled()) {
@@ -897,29 +953,6 @@
mSearchEditText.setOnEditorActionListener(this);
mSearchEditText.setText(mInitialFilter);
}
-
- private String getContactDisplayName(long contactId) {
- String contactName = null;
- Cursor c = getContentResolver().query(
- ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
- new String[] {Contacts.DISPLAY_NAME}, null, null, null);
- try {
- if (c != null && c.moveToFirst()) {
- contactName = c.getString(0);
- }
- } finally {
- if (c != null) {
- c.close();
- }
- }
-
- if (contactName == null) {
- contactName = "";
- }
-
- return contactName;
- }
-
private int getSummaryDisplayNameColumnIndex() {
if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
return SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
@@ -942,66 +975,16 @@
}
break;
}
+ case R.id.done:
+ setMultiPickerResult();
+ finish();
+ break;
+ case R.id.revert:
+ finish();
+ break;
}
}
- private void setEmptyText() {
- if (mMode == MODE_JOIN_CONTACT || mSearchMode) {
- return;
- }
-
- TextView empty = (TextView) findViewById(R.id.emptyText);
- if (mDisplayOnlyPhones) {
- empty.setText(getText(R.string.noContactsWithPhoneNumbers));
- } else if (mMode == MODE_STREQUENT || mMode == MODE_STARRED) {
- empty.setText(getText(R.string.noFavoritesHelpText));
- } else if (mMode == MODE_QUERY || mMode == MODE_QUERY_PICK
- || mMode == MODE_QUERY_PICK_PHONE || mMode == MODE_QUERY_PICK_TO_VIEW
- || mMode == MODE_QUERY_PICK_TO_EDIT) {
- empty.setText(getText(R.string.noMatchingContacts));
- } else {
- boolean hasSim = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE))
- .hasIccCard();
- boolean createShortcut = Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction());
- if (isSyncActive()) {
- if (createShortcut) {
- // Help text is the same no matter whether there is SIM or not.
- empty.setText(getText(R.string.noContactsHelpTextWithSyncForCreateShortcut));
- } else if (hasSim) {
- empty.setText(getText(R.string.noContactsHelpTextWithSync));
- } else {
- empty.setText(getText(R.string.noContactsNoSimHelpTextWithSync));
- }
- } else {
- if (createShortcut) {
- // Help text is the same no matter whether there is SIM or not.
- empty.setText(getText(R.string.noContactsHelpTextForCreateShortcut));
- } else if (hasSim) {
- empty.setText(getText(R.string.noContactsHelpText));
- } else {
- empty.setText(getText(R.string.noContactsNoSimHelpText));
- }
- }
- }
- }
-
- private boolean isSyncActive() {
- Account[] accounts = AccountManager.get(this).getAccounts();
- if (accounts != null && accounts.length > 0) {
- IContentService contentService = ContentResolver.getContentService();
- for (Account account : accounts) {
- try {
- if (contentService.isSyncActive(account, ContactsContract.AUTHORITY)) {
- return true;
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Could not get the sync status");
- }
- }
- }
- return false;
- }
-
private void buildUserGroupUri(String group) {
mGroupUri = Uri.withAppendedPath(Contacts.CONTENT_GROUP_URI, group);
}
@@ -1159,7 +1142,7 @@
retryUpgrade.setOnClickListener(listener);
}
- private String getTextFilter() {
+ protected String getTextFilter() {
if (mSearchEditText != null) {
return mSearchEditText.getText().toString();
}
@@ -1191,6 +1174,9 @@
// Save list state in the bundle so we can restore it after the QueryHandler has run
if (mList != null) {
icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState());
+ if (mMode == MODE_PICK_MULTIPLE_PHONES && mUserSelection != null) {
+ mUserSelection.saveInstanceState(icicle);
+ }
}
}
@@ -1199,13 +1185,15 @@
super.onRestoreInstanceState(icicle);
// Retrieve list state. This will be applied after the QueryHandler has run
mListState = icicle.getParcelable(LIST_STATE_KEY);
+ if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+ mUserSelection = new UserSelection(icicle);
+ }
}
@Override
protected void onStop() {
super.onStop();
- mAdapter.setSuggestionsCursor(null);
mAdapter.changeCursor(null);
if (mMode == MODE_QUERY) {
@@ -1219,6 +1207,12 @@
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
+ if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+ final MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.pick, menu);
+ return true;
+ }
+
// If Contacts was invoked by another Activity simply as a way of
// picking a contact, don't show the options menu
if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
@@ -1232,6 +1226,26 @@
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
+ if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+ if (mShowSelectedOnly) {
+ menu.findItem(R.id.menu_display_selected).setVisible(false);
+ menu.findItem(R.id.menu_display_all).setVisible(true);
+ menu.findItem(R.id.menu_select_all).setVisible(false);
+ menu.findItem(R.id.menu_select_none).setVisible(false);
+ return true;
+ }
+ menu.findItem(R.id.menu_display_all).setVisible(false);
+ menu.findItem(R.id.menu_display_selected).setVisible(true);
+ if (mUserSelection.isAllSelected()) {
+ menu.findItem(R.id.menu_select_all).setVisible(false);
+ menu.findItem(R.id.menu_select_none).setVisible(true);
+ } else {
+ menu.findItem(R.id.menu_select_all).setVisible(true);
+ menu.findItem(R.id.menu_select_none).setVisible(false);
+ }
+ return true;
+ }
+
final boolean defaultMode = (mMode == MODE_DEFAULT);
menu.findItem(R.id.menu_display_groups).setVisible(defaultMode);
return true;
@@ -1266,6 +1280,28 @@
startActivity(intent);
return true;
}
+ case R.id.menu_select_all: {
+ mUserSelection.setAllPhonesSelected(true);
+ checkAll(true);
+ updateWidgets(true);
+ return true;
+ }
+ case R.id.menu_select_none: {
+ mUserSelection.setAllPhonesSelected(false);
+ checkAll(false);
+ updateWidgets(true);
+ return true;
+ }
+ case R.id.menu_display_selected: {
+ mShowSelectedOnly = true;
+ startQuery();
+ return true;
+ }
+ case R.id.menu_display_all: {
+ mShowSelectedOnly = false;
+ startQuery();
+ return true;
+ }
}
return false;
}
@@ -1282,8 +1318,16 @@
} else {
if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0) {
if ((mMode & MODE_MASK_PICKER) != 0) {
+ Bundle extras = null;
+ if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+ extras = getIntent().getExtras();
+ if (extras == null) {
+ extras = new Bundle();
+ }
+ mUserSelection.fillSelectionForSearchMode(extras);
+ }
ContactsSearchManager.startSearchForResult(this, initialQuery,
- SUBACTIVITY_FILTER);
+ SUBACTIVITY_FILTER, extras);
} else {
ContactsSearchManager.startSearch(this, initialQuery);
}
@@ -1296,9 +1340,6 @@
* search text edit.
*/
protected void onSearchTextChanged() {
- // Set the proper empty string
- setEmptyText();
-
Filter filter = mAdapter.getFilter();
filter.filter(getTextFilter());
}
@@ -1306,7 +1347,7 @@
/**
* Starts a new activity that will run a search query and display search results.
*/
- private void doSearch() {
+ protected void doSearch() {
String query = getTextFilter();
if (TextUtils.isEmpty(query)) {
return;
@@ -1549,6 +1590,10 @@
if (resultCode == RESULT_OK) {
setResult(RESULT_OK, data);
finish();
+ } else if (resultCode == RESULT_CANCELED && mMode == MODE_PICK_MULTIPLE_PHONES) {
+ // Finish the activity if the sub activity was canceled as back key is used
+ // to confirm user selection in MODE_PICK_MULTIPLE_PHONES.
+ finish();
}
break;
}
@@ -1730,6 +1775,14 @@
return false;
}
+ @Override
+ public void onBackPressed() {
+ if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+ setMultiPickerResult();
+ }
+ super.onBackPressed();
+ }
+
/**
* Prompt the user before deleting the given {@link Contacts} entry.
*/
@@ -1808,6 +1861,10 @@
protected void onListItemClick(ListView l, View v, int position, long id) {
hideSoftKeyboard();
+ onListItemClick(position, id);
+ }
+
+ protected void onListItemClick(int position, long id) {
if (mSearchMode && mAdapter.isSearchAllContactsItemPosition(position)) {
doSearch();
} else if (mMode == MODE_INSERT_OR_EDIT_CONTACT || mMode == MODE_QUERY_PICK_TO_EDIT) {
@@ -1830,16 +1887,11 @@
&& position == 0) {
Intent newContact = new Intent(Intents.Insert.ACTION, Contacts.CONTENT_URI);
startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);
- } else if (mMode == MODE_JOIN_CONTACT && id == JOIN_MODE_SHOW_ALL_CONTACTS_ID) {
- mJoinModeShowAllContacts = false;
- startQuery();
} else if (id > 0) {
final Uri uri = getSelectedUri(position);
if ((mMode & MODE_MASK_PICKER) == 0) {
final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivityForResult(intent, SUBACTIVITY_VIEW_CONTACT);
- } else if (mMode == MODE_JOIN_CONTACT) {
- returnPickerResult(null, null, uri);
} else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
// Started with query that should launch to view contact
final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
@@ -1872,7 +1924,7 @@
* @param selectedUri In most cases, this should be a lookup {@link Uri}, possibly
* generated through {@link Contacts#getLookupUri(long, String)}.
*/
- private void returnPickerResult(Cursor c, String name, Uri selectedUri) {
+ protected void returnPickerResult(Cursor c, String name, Uri selectedUri) {
final Intent intent = new Intent();
if (mShortcutAction != null) {
@@ -2078,10 +2130,8 @@
}
}
- private Uri getUriToQuery() {
+ protected Uri getUriToQuery() {
switch(mMode) {
- case MODE_JOIN_CONTACT:
- return getJoinSuggestionsUri(null);
case MODE_FREQUENT:
case MODE_STARRED:
return Contacts.CONTENT_URI;
@@ -2099,6 +2149,7 @@
case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
return People.CONTENT_URI;
}
+ case MODE_PICK_MULTIPLE_PHONES:
case MODE_PICK_PHONE: {
return buildSectionIndexerUri(Phone.CONTENT_URI);
}
@@ -2173,7 +2224,7 @@
* Build the {@link Uri} for the given {@link ListView} position, which can
* be used as result when in {@link #MODE_MASK_PICKER} mode.
*/
- private Uri getSelectedUri(int position) {
+ protected Uri getSelectedUri(int position) {
if (position == ListView.INVALID_POSITION) {
throw new IllegalArgumentException("Position not in list bounds");
}
@@ -2205,7 +2256,6 @@
String[] getProjectionForQuery() {
switch(mMode) {
- case MODE_JOIN_CONTACT:
case MODE_STREQUENT:
case MODE_FREQUENT:
case MODE_STARRED:
@@ -2228,6 +2278,7 @@
return LEGACY_PEOPLE_PROJECTION ;
}
case MODE_QUERY_PICK_PHONE:
+ case MODE_PICK_MULTIPLE_PHONES:
case MODE_PICK_PHONE: {
return PHONES_PROJECTION;
}
@@ -2314,7 +2365,7 @@
}
}
- private Uri getContactFilterUri(String filter) {
+ protected Uri getContactFilterUri(String filter) {
Uri baseUri;
if (!TextUtils.isEmpty(filter)) {
baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter));
@@ -2342,39 +2393,40 @@
.appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
}
- private Uri getJoinSuggestionsUri(String filter) {
- Builder builder = Contacts.CONTENT_URI.buildUpon();
- builder.appendEncodedPath(String.valueOf(mQueryAggregateId));
- builder.appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY);
- if (!TextUtils.isEmpty(filter)) {
- builder.appendEncodedPath(Uri.encode(filter));
- }
- builder.appendQueryParameter("limit", String.valueOf(MAX_SUGGESTIONS));
- return builder.build();
- }
- private String getSortOrder(String[] projectionType) {
+ protected String getSortOrder(String[] projectionType) {
+ String sortKey;
if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
- return Contacts.SORT_KEY_PRIMARY;
+ sortKey = Contacts.SORT_KEY_PRIMARY;
} else {
- return Contacts.SORT_KEY_ALTERNATIVE;
+ sortKey = Contacts.SORT_KEY_ALTERNATIVE;
}
+ switch (mMode) {
+ case MODE_LEGACY_PICK_PERSON:
+ case MODE_LEGACY_PICK_OR_CREATE_PERSON:
+ sortKey = Contacts.DISPLAY_NAME;
+ break;
+ case MODE_LEGACY_PICK_PHONE:
+ sortKey = People.DISPLAY_NAME;
+ break;
+ }
+ return sortKey;
}
void startQuery() {
- // Set the proper empty string
- setEmptyText();
-
if (mSearchResultsMode) {
TextView foundContactsText = (TextView)findViewById(R.id.search_results_found);
foundContactsText.setText(R.string.search_results_searching);
}
+ if (mEmptyView != null) {
+ mEmptyView.hide();
+ }
+
mAdapter.setLoading(true);
// Cancel any pending queries
mQueryHandler.cancelOperation(QUERY_TOKEN);
- mQueryHandler.setLoadingJoinSuggestions(false);
mSortOrder = mContactsPrefs.getSortOrder();
mDisplayOrder = mContactsPrefs.getDisplayOrder();
@@ -2405,6 +2457,10 @@
.build();
}
+ startQuery(uri, projection);
+ }
+
+ protected void startQuery(Uri uri, String[] projection) {
// Kick off the new query
switch (mMode) {
case MODE_GROUP:
@@ -2447,6 +2503,25 @@
mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, null);
break;
+ case MODE_PICK_MULTIPLE_PHONES:
+ // Filter unknown phone numbers first.
+ mPhoneNumberAdapter.doFilter(null, mShowSelectedOnly);
+ if (mShowSelectedOnly) {
+ StringBuilder idSetBuilder = new StringBuilder();
+ Iterator<Long> itr = mUserSelection.getSelectedPhonIds();
+ if (itr.hasNext()) {
+ idSetBuilder.append(Long.toString(itr.next()));
+ }
+ while (itr.hasNext()) {
+ idSetBuilder.append(',');
+ idSetBuilder.append(Long.toString(itr.next()));
+ }
+ String whereClause = Phone._ID + " IN (" + idSetBuilder.toString() + ")";
+ mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
+ projection, whereClause, null, getSortOrder(projection));
+ break;
+ }
+ // Fall through For other cases
case MODE_PICK_PHONE:
case MODE_LEGACY_PICK_PHONE:
mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
@@ -2459,15 +2534,15 @@
ContactMethods.KIND + "=" + android.provider.Contacts.KIND_POSTAL, null,
getSortOrder(projection));
break;
-
- case MODE_JOIN_CONTACT:
- mQueryHandler.setLoadingJoinSuggestions(true);
- mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection,
- null, null, null);
- break;
}
}
+ protected void startQuery(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, selection, selectionArgs,
+ sortOrder);
+ }
+
/**
* Called from a background thread to do the filter and return the resulting cursor.
*
@@ -2520,6 +2595,10 @@
return resolver.query(uri, projection, null, null, null);
}
+ case MODE_PICK_MULTIPLE_PHONES:
+ // Filter phone numbers as well.
+ mPhoneNumberAdapter.doFilter(filter, mShowSelectedOnly);
+ // Fall through
case MODE_PICK_PHONE: {
Uri uri = getUriToQuery();
if (!TextUtils.isEmpty(filter)) {
@@ -2533,30 +2612,10 @@
//TODO: Support filtering here (bug 2092503)
break;
}
-
- case MODE_JOIN_CONTACT: {
-
- // We are on a background thread. Run queries one after the other synchronously
- Cursor cursor = resolver.query(getJoinSuggestionsUri(filter), projection, null,
- null, null);
- mAdapter.setSuggestionsCursor(cursor);
- mJoinModeShowAllContacts = false;
- return resolver.query(getContactFilterUri(filter), projection,
- Contacts._ID + " != " + mQueryAggregateId + " AND " + CLAUSE_ONLY_VISIBLE,
- null, getSortOrder(projection));
- }
}
throw new UnsupportedOperationException("filtering not allowed in mode " + mMode);
}
- private Cursor getShowAllContactsLabelCursor(String[] projection) {
- MatrixCursor matrixCursor = new MatrixCursor(projection);
- Object[] row = new Object[projection.length];
- // The only columns we care about is the id
- row[SUMMARY_ID_COLUMN_INDEX] = JOIN_MODE_SHOW_ALL_CONTACTS_ID;
- matrixCursor.addRow(row);
- return matrixCursor;
- }
/**
* Calls the currently selected list item.
@@ -2695,56 +2754,160 @@
return (Cursor) listView.getAdapter().getItem(index);
}
- private static class QueryHandler extends AsyncQueryHandler {
+ private void initMultiPicker(final Intent intent) {
+ final Handler handler = new Handler();
+ // TODO : Shall we still show the progressDialog in search mode.
+ final ProgressDialog progressDialog = new ProgressDialog(this);
+ progressDialog.setMessage(getText(R.string.adding_recipients));
+ progressDialog.setIndeterminate(true);
+ progressDialog.setCancelable(false);
+
+ final Runnable showProgress = new Runnable() {
+ public void run() {
+ progressDialog.show();
+ }
+ };
+ handler.postDelayed(showProgress, 1);
+
+ new Thread(new Runnable() {
+ public void run() {
+ try {
+ loadSelectionFromIntent(intent);
+ } finally {
+ handler.removeCallbacks(showProgress);
+ progressDialog.dismiss();
+ }
+ final Runnable populateWorker = new Runnable() {
+ public void run() {
+ if (mAdapter != null) {
+ mAdapter.notifyDataSetChanged();
+ }
+ updateWidgets(false);
+ }
+ };
+ handler.post(populateWorker);
+ }
+ }).start();
+ }
+
+ private void getPhoneNumbersOrIdsFromURIs(final Parcelable[] uris,
+ final List<String> phoneNumbers, final List<Long> phoneIds) {
+ if (uris != null) {
+ for (Parcelable paracelable : uris) {
+ Uri uri = (Uri) paracelable;
+ if (uri == null) continue;
+ String scheme = uri.getScheme();
+ if (phoneNumbers != null && TEL_SCHEME.equals(scheme)) {
+ phoneNumbers.add(uri.getSchemeSpecificPart());
+ } else if (phoneIds != null && CONTENT_SCHEME.equals(scheme)) {
+ phoneIds.add(ContentUris.parseId(uri));
+ }
+ }
+ }
+ }
+
+ private void loadSelectionFromIntent(Intent intent) {
+ Parcelable[] uris = intent.getParcelableArrayExtra(Intents.EXTRA_PHONE_URIS);
+ ArrayList<String> phoneNumbers = new ArrayList<String>();
+ ArrayList<Long> phoneIds = new ArrayList<Long>();
+ ArrayList<String> selectedPhoneNumbers = null;
+ if (mSearchMode) {
+ // All selection will be read from EXTRA_SELECTION
+ getPhoneNumbersOrIdsFromURIs(uris, phoneNumbers, null);
+ uris = intent.getParcelableArrayExtra(UserSelection.EXTRA_SELECTION);
+ if (uris != null) {
+ selectedPhoneNumbers = new ArrayList<String>();
+ getPhoneNumbersOrIdsFromURIs(uris, selectedPhoneNumbers, phoneIds);
+ }
+ } else {
+ getPhoneNumbersOrIdsFromURIs(uris, phoneNumbers, phoneIds);
+ selectedPhoneNumbers = phoneNumbers;
+ }
+ mPhoneNumberAdapter = new PhoneNumberAdapter(this, phoneNumbers);
+ mUserSelection = new UserSelection(selectedPhoneNumbers, phoneIds);
+ }
+
+ private void setMultiPickerResult() {
+ setResult(RESULT_OK, mUserSelection.createSelectionIntent());
+ }
+
+ /**
+ * Go through the cursor and assign the chip color to contact who has more than one phone
+ * numbers.
+ * Assume the cursor is sorted by CONTACT_ID.
+ */
+ private void updateChipColor(Cursor cursor) {
+ if (cursor == null || cursor.getCount() == 0) {
+ return;
+ }
+ mContactColor.clear();
+ int backupPos = cursor.getPosition();
+ cursor.moveToFirst();
+ int color = 0;
+ long prevContactId = cursor.getLong(PHONE_CONTACT_ID_COLUMN_INDEX);
+ while (cursor.moveToNext()) {
+ long contactId = cursor.getLong(PHONE_CONTACT_ID_COLUMN_INDEX);
+ if (prevContactId == contactId) {
+ if (mContactColor.indexOfKey(Long.valueOf(contactId).hashCode()) < 0) {
+ mContactColor.put(Long.valueOf(contactId).hashCode(), CHIP_COLOR_ARRAY[color]);
+ color++;
+ if (color >= CHIP_COLOR_ARRAY.length) {
+ color = 0;
+ }
+ }
+ }
+ prevContactId = contactId;
+ }
+ cursor.moveToPosition(backupPos);
+ }
+
+ /**
+ * Get assigned chip color resource id for a given contact, 0 is returned if there is no mapped
+ * resource.
+ */
+ private int getChipColor(long contactId) {
+ return mContactColor.get(Long.valueOf(contactId).hashCode());
+ }
+
+ private void updateWidgets(boolean changed) {
+ int selected = mUserSelection.selectedCount();
+
+ if (selected >= 1) {
+ final String format =
+ getResources().getQuantityString(R.plurals.multiple_picker_title, selected);
+ setTitle(String.format(format, selected));
+ } else {
+ setTitle(getString(R.string.contactsList));
+ }
+
+ if (changed && mFooterView.getVisibility() == View.GONE) {
+ mFooterView.setVisibility(View.VISIBLE);
+ mFooterView.startAnimation(AnimationUtils.loadAnimation(this, R.anim.footer_appear));
+ }
+ }
+
+ private void checkAll(boolean checked) {
+ final ListView listView = getListView();
+ int childCount = listView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ContactListItemView child = (ContactListItemView)listView.getChildAt(i);
+ child.getCheckBoxView().setChecked(checked);
+ }
+ }
+
+ private class QueryHandler extends AsyncQueryHandler {
protected final WeakReference<ContactsListActivity> mActivity;
- protected boolean mLoadingJoinSuggestions = false;
public QueryHandler(Context context) {
super(context.getContentResolver());
mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context);
}
- public void setLoadingJoinSuggestions(boolean flag) {
- mLoadingJoinSuggestions = flag;
- }
-
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
final ContactsListActivity activity = mActivity.get();
if (activity != null && !activity.isFinishing()) {
-
- // Whenever we get a suggestions cursor, we need to immediately kick off
- // another query for the complete list of contacts
- if (cursor != null && mLoadingJoinSuggestions) {
- mLoadingJoinSuggestions = false;
- if (cursor.getCount() > 0) {
- activity.mAdapter.setSuggestionsCursor(cursor);
- } else {
- cursor.close();
- activity.mAdapter.setSuggestionsCursor(null);
- }
-
- if (activity.mAdapter.mSuggestionsCursorCount == 0
- || !activity.mJoinModeShowAllContacts) {
- startQuery(QUERY_TOKEN, null, activity.getContactFilterUri(
- activity.getTextFilter()),
- CONTACTS_SUMMARY_PROJECTION,
- Contacts._ID + " != " + activity.mQueryAggregateId
- + " AND " + CLAUSE_ONLY_VISIBLE, null,
- activity.getSortOrder(CONTACTS_SUMMARY_PROJECTION));
- return;
- }
-
- cursor = activity.getShowAllContactsLabelCursor(CONTACTS_SUMMARY_PROJECTION);
- }
-
- activity.mAdapter.changeCursor(cursor);
-
- // Now that the cursor is populated again, it's possible to restore the list state
- if (activity.mListState != null) {
- activity.mList.onRestoreInstanceState(activity.mListState);
- activity.mListState = null;
- }
+ activity.onQueryComplete(cursor);
} else {
if (cursor != null) {
cursor.close();
@@ -2753,12 +2916,25 @@
}
}
+ protected void onQueryComplete(Cursor cursor) {
+ mAdapter.changeCursor(cursor);
+
+ // Now that the cursor is populated again, it's possible to restore the list state
+ if (mListState != null) {
+ mList.onRestoreInstanceState(mListState);
+ mListState = null;
+ }
+ }
+
final static class ContactListItemCache {
public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
public CharArrayBuffer dataBuffer = new CharArrayBuffer(128);
public CharArrayBuffer highlightedTextBuffer = new CharArrayBuffer(128);
public TextWithHighlighting textWithHighlighting;
public CharArrayBuffer phoneticNameBuffer = new CharArrayBuffer(128);
+ public long phoneId;
+ // phoneNumber only validates when phoneId = INVALID_PHONE_ID
+ public String phoneNumber;
}
final static class PinnedHeaderCache {
@@ -2767,7 +2943,7 @@
public Drawable background;
}
- private final class ContactItemListAdapter extends CursorAdapter
+ protected class ContactItemListAdapter extends CursorAdapter
implements SectionIndexer, OnScrollListener, PinnedHeaderListView.PinnedHeaderAdapter {
private SectionIndexer mIndexer;
private boolean mLoading = true;
@@ -2777,8 +2953,6 @@
private boolean mDisplayAdditionalData = true;
private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
private boolean mDisplaySectionHeaders = true;
- private Cursor mSuggestionsCursor;
- private int mSuggestionsCursorCount;
public ContactItemListAdapter(Context context) {
super(context, null, false);
@@ -2824,14 +2998,6 @@
return mDisplaySectionHeaders;
}
- public void setSuggestionsCursor(Cursor cursor) {
- if (mSuggestionsCursor != null) {
- mSuggestionsCursor.close();
- }
- mSuggestionsCursor = cursor;
- mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount();
- }
-
/**
* Callback on the UI thread when the content observer on the backing cursor fires.
* Instead of calling requery we need to do an async query so that the requery doesn't
@@ -2882,10 +3048,6 @@
return IGNORE_ITEM_VIEW_TYPE;
}
- if (isShowAllContactsItemPosition(position)) {
- return IGNORE_ITEM_VIEW_TYPE;
- }
-
if (isSearchAllContactsItemPosition(position)) {
return IGNORE_ITEM_VIEW_TYPE;
}
@@ -2894,7 +3056,9 @@
// We don't want the separator view to be recycled.
return IGNORE_ITEM_VIEW_TYPE;
}
-
+ if (mMode == MODE_PICK_MULTIPLE_PHONES && position < mPhoneNumberAdapter.getCount()) {
+ return mPhoneNumberAdapter.getItemViewType(position);
+ }
return super.getItemViewType(position);
}
@@ -2915,11 +3079,6 @@
return getLayoutInflater().inflate(R.layout.create_new_contact, parent, false);
}
- if (isShowAllContactsItemPosition(position)) {
- return getLayoutInflater().
- inflate(R.layout.contacts_list_show_all_item, parent, false);
- }
-
if (isSearchAllContactsItemPosition(position)) {
return getLayoutInflater().
inflate(R.layout.contacts_list_search_all_item, parent, false);
@@ -2934,18 +3093,13 @@
return view;
}
- boolean showingSuggestion;
- Cursor cursor;
- if (mSuggestionsCursorCount != 0 && position < mSuggestionsCursorCount + 2) {
- showingSuggestion = true;
- cursor = mSuggestionsCursor;
- } else {
- showingSuggestion = false;
- cursor = mCursor;
+ // Check whether this view should be retrieved from mPhoneNumberAdapter
+ if (mMode == MODE_PICK_MULTIPLE_PHONES && position < mPhoneNumberAdapter.getCount()) {
+ return mPhoneNumberAdapter.getView(position, convertView, parent);
}
int realPosition = getRealPosition(position);
- if (!cursor.moveToPosition(realPosition)) {
+ if (!mCursor.moveToPosition(realPosition)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
@@ -2953,13 +3107,13 @@
View v;
if (convertView == null || convertView.getTag() == null) {
newView = true;
- v = newView(mContext, cursor, parent);
+ v = newView(mContext, mCursor, parent);
} else {
newView = false;
v = convertView;
}
- bindView(v, mContext, cursor);
- bindSectionHeader(v, realPosition, mDisplaySectionHeaders && !showingSuggestion);
+ bindView(v, mContext, mCursor);
+ bindSectionHeader(v, realPosition, mDisplaySectionHeaders);
return v;
}
@@ -2988,13 +3142,8 @@
return view;
}
- private boolean isShowAllContactsItemPosition(int position) {
- return mMode == MODE_JOIN_CONTACT && mJoinModeShowAllContacts
- && mSuggestionsCursorCount != 0 && position == mSuggestionsCursorCount + 2;
- }
-
private boolean isSearchAllContactsItemPosition(int position) {
- return mSearchMode && position == getCount() - 1;
+ return mSearchMode && mMode != MODE_PICK_MULTIPLE_PHONES && position == getCount() - 1;
}
private int getSeparatorId(int position) {
@@ -3002,13 +3151,6 @@
if (position == mFrequentSeparatorPos) {
separatorId = R.string.favoritesFrquentSeparator;
}
- if (mSuggestionsCursorCount != 0) {
- if (position == 0) {
- separatorId = R.string.separatorJoinAggregateSuggestions;
- } else if (position == mSuggestionsCursorCount + 1) {
- separatorId = R.string.separatorJoinAggregateAll;
- }
- }
return separatorId;
}
@@ -3016,6 +3158,7 @@
public View newView(Context context, Cursor cursor, ViewGroup parent) {
final ContactListItemView view = new ContactListItemView(context, null);
view.setOnCallButtonClickListener(ContactsListActivity.this);
+ view.setOnCheckBoxClickListener(mCheckBoxClickerListener);
view.setTag(new ContactListItemCache());
return view;
}
@@ -3031,9 +3174,11 @@
int defaultType;
int nameColumnIndex;
int phoneticNameColumnIndex;
+ int photoColumnIndex = SUMMARY_PHOTO_ID_COLUMN_INDEX;
boolean displayAdditionalData = mDisplayAdditionalData;
boolean highlightingEnabled = false;
switch(mMode) {
+ case MODE_PICK_MULTIPLE_PHONES:
case MODE_PICK_PHONE:
case MODE_LEGACY_PICK_PHONE:
case MODE_QUERY_PICK_PHONE: {
@@ -3043,6 +3188,7 @@
typeColumnIndex = PHONE_TYPE_COLUMN_INDEX;
labelColumnIndex = PHONE_LABEL_COLUMN_INDEX;
defaultType = Phone.TYPE_HOME;
+ photoColumnIndex = PHONE_PHOTO_ID_COLUMN_INDEX;
break;
}
case MODE_PICK_POSTAL:
@@ -3067,6 +3213,15 @@
}
}
+ if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+ cache.phoneId = Long.valueOf(cursor.getLong(PHONE_ID_COLUMN_INDEX));
+ CheckBox checkBox = view.getCheckBoxView();
+ checkBox.setChecked(mUserSelection.isSelected(cache.phoneId));
+ checkBox.setTag(cache);
+ int color = getChipColor(cursor.getLong(PHONE_CONTACT_ID_COLUMN_INDEX));
+ view.getChipView().setBackgroundResource(color);
+ }
+
// Set the name
cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer);
TextView nameView = view.getNameTextView();
@@ -3086,11 +3241,9 @@
nameView.setText(mUnknownNameText);
}
- boolean hasPhone = cursor.getColumnCount() >= SUMMARY_HAS_PHONE_COLUMN_INDEX
- && cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
-
// Make the call button visible if requested.
- if (mDisplayCallButton && hasPhone) {
+ if (mDisplayCallButton && cursor.getColumnCount() > SUMMARY_HAS_PHONE_COLUMN_INDEX
+ && cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0) {
int pos = cursor.getPosition();
view.showCallButton(android.R.id.button1, pos);
} else {
@@ -3102,8 +3255,8 @@
boolean useQuickContact = (mMode & MODE_MASK_DISABLE_QUIKCCONTACT) == 0;
long photoId = 0;
- if (!cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) {
- photoId = cursor.getLong(SUMMARY_PHOTO_ID_COLUMN_INDEX);
+ if (!cursor.isNull(photoColumnIndex)) {
+ photoId = cursor.getLong(photoColumnIndex);
}
ImageView viewToUse;
@@ -3236,7 +3389,7 @@
textView.setText(textWithHighlighting);
}
- private void bindSectionHeader(View itemView, int position, boolean displaySectionHeaders) {
+ protected void bindSectionHeader(View itemView, int position, boolean displaySectionHeaders) {
final ContactListItemView view = (ContactListItemView)itemView;
final ContactListItemCache cache = (ContactListItemCache) view.getTag();
if (!displaySectionHeaders) {
@@ -3292,9 +3445,25 @@
foundContactsText.setText(text);
}
+ if (mEmptyView != null && (cursor == null || cursor.getCount() == 0)) {
+ mEmptyView.show(mSearchMode, mDisplayOnlyPhones,
+ mMode == MODE_STREQUENT || mMode == MODE_STARRED,
+ mMode == MODE_QUERY || mMode == MODE_QUERY_PICK
+ || mMode == MODE_QUERY_PICK_PHONE || mMode == MODE_QUERY_PICK_TO_VIEW
+ || mMode == MODE_QUERY_PICK_TO_EDIT,
+ mShortcutAction != null,
+ mMode == MODE_PICK_MULTIPLE_PHONES,
+ mShowSelectedOnly);
+ }
+
super.changeCursor(cursor);
+
// Update the indexer for the fast scroll widget
updateIndexer(cursor);
+
+ if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+ updateChipColor(cursor);
+ }
}
private void updateIndexer(Cursor cursor) {
@@ -3350,8 +3519,7 @@
@Override
public boolean areAllItemsEnabled() {
return mMode != MODE_STARRED
- && !mShowNumberOfContacts
- && mSuggestionsCursorCount == 0;
+ && !mShowNumberOfContacts;
}
@Override
@@ -3362,10 +3530,6 @@
}
position--;
}
-
- if (mSuggestionsCursorCount > 0) {
- return position != 0 && position != mSuggestionsCursorCount + 1;
- }
return position != mFrequentSeparatorPos;
}
@@ -3382,7 +3546,7 @@
superCount++;
}
- if (mSearchMode) {
+ if (mSearchMode && mMode != MODE_PICK_MULTIPLE_PHONES) {
// Last element in the list is the "Find
superCount++;
}
@@ -3393,12 +3557,11 @@
superCount++;
}
- if (mSuggestionsCursorCount != 0) {
- // When showing suggestions, we have 2 additional list items: the "Suggestions"
- // and "All contacts" headers.
- return mSuggestionsCursorCount + superCount + 2;
+ if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+ superCount += mPhoneNumberAdapter.getCount();
}
- else if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
+
+ if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
// When showing strequent list, we have an additional list item - the separator.
return superCount + 1;
} else {
@@ -3420,19 +3583,13 @@
if ((mMode & MODE_MASK_CREATE_NEW) != 0 && !mSearchMode) {
return pos - 1;
- } else if (mSuggestionsCursorCount != 0) {
- // When showing suggestions, we have 2 additional list items: the "Suggestions"
- // and "All contacts" separators.
- if (pos < mSuggestionsCursorCount + 2) {
- // We are in the upper partition (Suggestions). Adjusting for the "Suggestions"
- // separator.
- return pos - 1;
- } else {
- // We are in the lower partition (All contacts). Adjusting for the size
- // of the upper partition plus the two separators.
- return pos - mSuggestionsCursorCount - 2;
- }
- } else if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
+ }
+
+ if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+ pos -= mPhoneNumberAdapter.getCount();
+ }
+
+ if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
// No separator, identity map
return pos;
} else if (pos <= mFrequentSeparatorPos) {
@@ -3446,10 +3603,7 @@
@Override
public Object getItem(int pos) {
- if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) {
- mSuggestionsCursor.moveToPosition(getRealPosition(pos));
- return mSuggestionsCursor;
- } else if (isSearchAllContactsItemPosition(pos)){
+ if (isSearchAllContactsItemPosition(pos)){
return null;
} else {
int realPosition = getRealPosition(pos);
@@ -3462,13 +3616,7 @@
@Override
public long getItemId(int pos) {
- if (mSuggestionsCursorCount != 0 && pos < mSuggestionsCursorCount + 2) {
- if (mSuggestionsCursor.moveToPosition(pos - 1)) {
- return mSuggestionsCursor.getLong(mRowIDColumn);
- } else {
- return 0;
- }
- } else if (isSearchAllContactsItemPosition(pos)) {
+ if (isSearchAllContactsItemPosition(pos)) {
return 0;
}
int realPosition = getRealPosition(pos);
@@ -3565,4 +3713,332 @@
}
}
}
+
+ /**
+ * This class is the adapter for the phone numbers which may not be found in the contacts. It is
+ * called in ContactItemListAdapter in MODE_PICK_MULTIPLE_PHONES mode and shouldn't be a adapter
+ * for any View due to the missing implementation of getItem and getItemId.
+ */
+ private class PhoneNumberAdapter extends BaseAdapter {
+ public static final long INVALID_PHONE_ID = -1;
+
+ /** The initial phone numbers */
+ private List<String> mPhoneNumbers;
+
+ /** The phone numbers after the filtering */
+ private ArrayList<String> mFilteredPhoneNumbers = new ArrayList<String>();
+
+ private Context mContext;
+
+ /** The position where this Adapter Phone numbers start*/
+ private int mStartPos;
+
+ public PhoneNumberAdapter(Context context, final List<String> phoneNumbers) {
+ init(context, phoneNumbers);
+ }
+
+ private void init(Context context, final List<String> phoneNumbers) {
+ mStartPos = (mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0 ? 1 : 0;
+ mContext = context;
+ if (phoneNumbers != null) {
+ mFilteredPhoneNumbers.addAll(phoneNumbers);
+ mPhoneNumbers = phoneNumbers;
+ } else {
+ mPhoneNumbers = new ArrayList<String>();
+ }
+ }
+
+ public int getCount() {
+ int filteredCount = mFilteredPhoneNumbers.size();
+ if (filteredCount == 0) {
+ return 0;
+ }
+ // Count on the separator
+ return 1 + filteredCount;
+ }
+
+ public Object getItem(int position) {
+ // This method is not used currently.
+ throw new RuntimeException("This method is not implemented");
+ }
+
+ public long getItemId(int position) {
+ // This method is not used currently.
+ throw new RuntimeException("This method is not implemented");
+ }
+
+ /**
+ * @return the initial phone numbers, the zero length array is returned when there is no
+ * initial numbers.
+ */
+ public final List<String> getPhoneNumbers() {
+ return mPhoneNumbers;
+ }
+
+ /**
+ * @return the filtered phone numbers, the zero size ArrayList is returned when there is no
+ * initial numbers.
+ */
+ public ArrayList<String> getFilteredPhoneNumbers() {
+ return mFilteredPhoneNumbers;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ int viewCount = getCount();
+ if (viewCount == 0) {
+ return null;
+ }
+ // Separator
+ if (position == mStartPos) {
+ TextView view;
+ if (convertView != null && convertView instanceof TextView) {
+ view = (TextView) convertView;
+ } else {
+ LayoutInflater inflater =
+ (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ view = (TextView) inflater.inflate(R.layout.list_separator, parent, false);
+ }
+ view.setText(R.string.unknown_contacts_separator);
+ return view;
+ }
+ // PhoneNumbers start from position of startPos + 1
+ if (position >= mStartPos + 1 && position < mStartPos + viewCount) {
+ View view;
+ if (convertView != null && convertView.getTag() != null &&
+ convertView.getTag() instanceof ContactListItemCache) {
+ view = convertView;
+ } else {
+ view = mAdapter.newView(mContext, null, parent);
+ }
+ bindView(view, mFilteredPhoneNumbers.get(position - 1 - mStartPos));
+ return view;
+ }
+ return null;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return position == mStartPos ? IGNORE_ITEM_VIEW_TYPE : super.getItemViewType(position);
+ }
+
+ private void bindView(View view, final String label) {
+ ContactListItemView itemView = (ContactListItemView) view;
+ final ContactListItemCache cache = (ContactListItemCache) view.getTag();
+ itemView.getNameTextView().setText(label);
+ CheckBox checkBox = itemView.getCheckBoxView();
+ checkBox.setChecked(mUserSelection.isSelected(label));
+ itemView.getChipView().setBackgroundResource(0);
+ cache.phoneId = INVALID_PHONE_ID;
+ cache.phoneNumber = label;
+ checkBox.setTag(cache);
+ }
+
+ public void doFilter(final String constraint, boolean selectedOnly) {
+ if (mPhoneNumbers == null) {
+ return;
+ }
+ mFilteredPhoneNumbers.clear();
+ for (String number : mPhoneNumbers) {
+ if (selectedOnly && !mUserSelection.isSelected(number) ||
+ !TextUtils.isEmpty(constraint) && !number.startsWith(constraint)) {
+ continue;
+ }
+ mFilteredPhoneNumbers.add(number);
+ }
+ }
+ }
+
+ /**
+ * This class is used to keep the user's selection in MODE_PICK_MULTIPLE_PHONES mode.
+ */
+ private class UserSelection {
+ public static final String EXTRA_SELECTION =
+ "com.android.contacts.ContactsListActivity.UserSelection.extra.SELECTION";
+ private static final String SELECTED_UNKNOWN_PHONES_KEY = "selected_unknown_phones";
+ private static final String SELECTED_PHONE_IDS_KEY = "selected_phone_id";
+
+ /** The PHONE_ID of selected number in user contacts*/
+ private HashSet<Long> mSelectedPhoneIds = new HashSet<Long>();
+
+ /** The selected phone numbers in the PhoneNumberAdapter */
+ private HashSet<String> mSelectedPhoneNumbers = new HashSet<String>();
+
+ /**
+ * @param phoneNumbers the phone numbers are selected.
+ */
+ public UserSelection(final List<String> phoneNumbers, final List<Long> phoneIds) {
+ init(phoneNumbers, phoneIds);
+ }
+
+ /**
+ * Creates from a instance state.
+ */
+ public UserSelection (Bundle icicle) {
+ init(icicle.getStringArray(SELECTED_UNKNOWN_PHONES_KEY),
+ icicle.getLongArray(SELECTED_PHONE_IDS_KEY));
+ }
+
+ public void saveInstanceState(Bundle icicle) {
+ int selectedUnknownsCount = mSelectedPhoneNumbers.size();
+ if (selectedUnknownsCount > 0) {
+ String[] selectedUnknows = new String[selectedUnknownsCount];
+ icicle.putStringArray(SELECTED_UNKNOWN_PHONES_KEY,
+ mSelectedPhoneNumbers.toArray(selectedUnknows));
+ }
+ int selectedKnownsCount = mSelectedPhoneIds.size();
+ if (selectedKnownsCount > 0) {
+ long[] selectedPhoneIds = new long [selectedKnownsCount];
+ int index = 0;
+ for (Long phoneId : mSelectedPhoneIds) {
+ selectedPhoneIds[index++] = phoneId.longValue();
+ }
+ icicle.putLongArray(SELECTED_PHONE_IDS_KEY, selectedPhoneIds);
+
+ }
+ }
+
+ private void init(final String[] selecedUnknownNumbers, final long[] selectedPhoneIds) {
+ if (selecedUnknownNumbers != null) {
+ for (String number : selecedUnknownNumbers) {
+ setPhoneSelected(number, true);
+ }
+ }
+ if (selectedPhoneIds != null) {
+ for (long id : selectedPhoneIds) {
+ setPhoneSelected(id, true);
+ }
+ }
+ }
+
+ private void init(final List<String> selecedUnknownNumbers,
+ final List<Long> selectedPhoneIds) {
+ if (selecedUnknownNumbers != null) {
+ setPhoneNumbersSelected(selecedUnknownNumbers, true);
+ }
+ if (selectedPhoneIds != null) {
+ setPhoneIdsSelected(selectedPhoneIds, true);
+ }
+ }
+
+ private void setPhoneNumbersSelected(final List<String> phoneNumbers, boolean selected) {
+ if (selected) {
+ mSelectedPhoneNumbers.addAll(phoneNumbers);
+ } else {
+ mSelectedPhoneNumbers.removeAll(phoneNumbers);
+ }
+ }
+
+ private void setPhoneIdsSelected(final List<Long> phoneIds, boolean selected) {
+ if (selected) {
+ mSelectedPhoneIds.addAll(phoneIds);
+ } else {
+ mSelectedPhoneIds.removeAll(phoneIds);
+ }
+ }
+
+ public void setPhoneSelected(final String phoneNumber, boolean selected) {
+ if (!TextUtils.isEmpty(phoneNumber)) {
+ if (selected) {
+ mSelectedPhoneNumbers.add(phoneNumber);
+ } else {
+ mSelectedPhoneNumbers.remove(phoneNumber);
+ }
+ }
+ }
+
+ public void setPhoneSelected(long phoneId, boolean selected) {
+ if (selected) {
+ mSelectedPhoneIds.add(phoneId);
+ } else {
+ mSelectedPhoneIds.remove(phoneId);
+ }
+ }
+
+ public boolean isSelected(long phoneId) {
+ return mSelectedPhoneIds.contains(phoneId);
+ }
+
+ public boolean isSelected(final String phoneNumber) {
+ return mSelectedPhoneNumbers.contains(phoneNumber);
+ }
+
+ public void setAllPhonesSelected(boolean selected) {
+ if (selected) {
+ Cursor cursor = mAdapter.getCursor();
+ if (cursor != null) {
+ int backupPos = cursor.getPosition();
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ setPhoneSelected(cursor.getLong(PHONE_ID_COLUMN_INDEX), true);
+ }
+ cursor.moveToPosition(backupPos);
+ }
+ for (String number : mPhoneNumberAdapter.getFilteredPhoneNumbers()) {
+ setPhoneSelected(number, true);
+ }
+ } else {
+ mSelectedPhoneIds.clear();
+ mSelectedPhoneNumbers.clear();
+ }
+ }
+
+ public boolean isAllSelected() {
+ return selectedCount() == mPhoneNumberAdapter.getFilteredPhoneNumbers().size()
+ + mAdapter.getCount();
+ }
+
+ public int selectedCount() {
+ return mSelectedPhoneNumbers.size() + mSelectedPhoneIds.size();
+ }
+
+ public Iterator<Long> getSelectedPhonIds() {
+ return mSelectedPhoneIds.iterator();
+ }
+
+ private int fillSelectedNumbers(Uri[] uris, int from) {
+ int count = mSelectedPhoneNumbers.size();
+ if (count == 0)
+ return from;
+ // Below loop keeps phone numbers by initial order.
+ List<String> phoneNumbers = mPhoneNumberAdapter.getPhoneNumbers();
+ for (String phoneNumber : phoneNumbers) {
+ if (isSelected(phoneNumber)) {
+ Uri.Builder ub = new Uri.Builder();
+ ub.scheme(TEL_SCHEME);
+ ub.encodedOpaquePart(phoneNumber);
+ uris[from++] = ub.build();
+ }
+ }
+ return from;
+ }
+
+ private int fillSelectedPhoneIds(Uri[] uris, int from) {
+ int count = mSelectedPhoneIds.size();
+ if (count == 0)
+ return from;
+ Iterator<Long> it = mSelectedPhoneIds.iterator();
+ while (it.hasNext()) {
+ uris[from++] = ContentUris.withAppendedId(Phone.CONTENT_URI, it.next());
+ }
+ return from;
+ }
+
+ private Uri[] getSelected() {
+ Uri[] uris = new Uri[mSelectedPhoneNumbers.size() + mSelectedPhoneIds.size()];
+ int from = fillSelectedNumbers(uris, 0);
+ fillSelectedPhoneIds(uris, from);
+ return uris;
+ }
+
+ public Intent createSelectionIntent() {
+ Intent intent = new Intent();
+ intent.putExtra(Intents.EXTRA_PHONE_URIS, getSelected());
+
+ return intent;
+ }
+
+ public void fillSelectionForSearchMode(Bundle bundle) {
+ bundle.putParcelableArray(EXTRA_SELECTION, getSelected());
+ }
+ }
}
diff --git a/src/com/android/contacts/ContactsSearchManager.java b/src/com/android/contacts/ContactsSearchManager.java
index d65e079..2297817 100644
--- a/src/com/android/contacts/ContactsSearchManager.java
+++ b/src/com/android/contacts/ContactsSearchManager.java
@@ -40,18 +40,26 @@
public static final String ORIGINAL_COMPONENT_EXTRA_KEY = "originalComponent";
/**
+ * An extra that provides context for search UI and defines the scope for
+ * the search queries.
+ */
+ public static final String ORIGINAL_TYPE_EXTRA_KEY = "originalType";
+
+ /**
* Starts the contact list activity in the search mode.
*/
public static void startSearch(Activity context, String initialQuery) {
- context.startActivity(buildIntent(context, initialQuery));
+ context.startActivity(buildIntent(context, initialQuery, null));
}
public static void startSearchForResult(Activity context, String initialQuery,
- int requestCode) {
- context.startActivityForResult(buildIntent(context, initialQuery), requestCode);
+ int requestCode, Bundle includedExtras) {
+ context.startActivityForResult(
+ buildIntent(context, initialQuery, includedExtras), requestCode);
}
- private static Intent buildIntent(Activity context, String initialQuery) {
+ private static Intent buildIntent(
+ Activity context, String initialQuery, Bundle includedExtras) {
Intent intent = new Intent();
intent.setData(ContactsContract.Contacts.CONTENT_URI);
intent.setAction(UI.FILTER_CONTACTS_ACTION);
@@ -64,6 +72,10 @@
intent.putExtra(UI.FILTER_TEXT_EXTRA_KEY, initialQuery);
intent.putExtra(ORIGINAL_ACTION_EXTRA_KEY, originalIntent.getAction());
intent.putExtra(ORIGINAL_COMPONENT_EXTRA_KEY, originalIntent.getComponent().getClassName());
+ intent.putExtra(ORIGINAL_TYPE_EXTRA_KEY, originalIntent.getType());
+ if (includedExtras != null) {
+ intent.putExtras(includedExtras);
+ }
return intent;
}
}
diff --git a/src/com/android/contacts/ImportVCardActivity.java b/src/com/android/contacts/ImportVCardActivity.java
index 0a324fe..85e75e6 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -39,6 +39,7 @@
import android.pim.vcard.VCardEntryCounter;
import android.pim.vcard.VCardInterpreter;
import android.pim.vcard.VCardInterpreterCollection;
+import android.pim.vcard.VCardParser;
import android.pim.vcard.VCardParser_V21;
import android.pim.vcard.VCardParser_V30;
import android.pim.vcard.VCardSourceDetector;
@@ -155,7 +156,7 @@
private class VCardReadThread extends Thread
implements DialogInterface.OnCancelListener {
private ContentResolver mResolver;
- private VCardParser_V21 mVCardParser;
+ private VCardParser mVCardParser;
private boolean mCanceled;
private PowerManager.WakeLock mWakeLock;
private Uri mUri;
@@ -220,13 +221,15 @@
boolean result;
try {
result = readOneVCardFile(targetUri,
- VCardConfig.DEFAULT_CHARSET, builderCollection, null, true, null);
+ VCardConfig.DEFAULT_IMPORT_CHARSET,
+ builderCollection, null, true, null);
} catch (VCardNestedException e) {
try {
// Assume that VCardSourceDetector was able to detect the source.
// Try again with the detector.
result = readOneVCardFile(targetUri,
- VCardConfig.DEFAULT_CHARSET, counter, detector, false, null);
+ VCardConfig.DEFAULT_IMPORT_CHARSET,
+ counter, detector, false, null);
} catch (VCardNestedException e2) {
result = false;
Log.e(LOG_TAG, "Must not reach here. " + e2);
@@ -265,7 +268,8 @@
VCardSourceDetector detector = new VCardSourceDetector();
try {
- if (!readOneVCardFile(targetUri, VCardConfig.DEFAULT_CHARSET,
+ if (!readOneVCardFile(targetUri,
+ VCardConfig.DEFAULT_IMPORT_CHARSET,
detector, null, true, mErrorFileNameList)) {
continue;
}
@@ -337,7 +341,7 @@
if (charset != null) {
builder = new VCardEntryConstructor(charset, charset, false, vcardType, mAccount);
} else {
- charset = VCardConfig.DEFAULT_CHARSET;
+ charset = VCardConfig.DEFAULT_IMPORT_CHARSET;
builder = new VCardEntryConstructor(null, null, false, vcardType, mAccount);
}
VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
diff --git a/src/com/android/contacts/JoinContactActivity.java b/src/com/android/contacts/JoinContactActivity.java
new file mode 100644
index 0000000..47c7547
--- /dev/null
+++ b/src/com/android/contacts/JoinContactActivity.java
@@ -0,0 +1,382 @@
+/*
+ * 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.
+ */
+
+package com.android.contacts;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.AggregationSuggestions;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/**
+ * An activity that shows a list of contacts that can be joined with the target contact.
+ */
+public class JoinContactActivity extends ContactsListActivity {
+
+ private static final String TAG = "JoinContactActivity";
+
+ /**
+ * The action for the join contact activity.
+ * <p>
+ * Input: extra field {@link #EXTRA_TARGET_CONTACT_ID} is the aggregate ID.
+ * TODO: move to {@link ContactsContract}.
+ */
+ public static final String JOIN_CONTACT = "com.android.contacts.action.JOIN_CONTACT";
+
+ /**
+ * Used with {@link #JOIN_CONTACT} to give it the target for aggregation.
+ * <p>
+ * Type: LONG
+ */
+ public static final String EXTRA_TARGET_CONTACT_ID = "com.android.contacts.action.CONTACT_ID";
+
+ /** Maximum number of suggestions shown for joining aggregates */
+ private static final int MAX_SUGGESTIONS = 4;
+
+ private long mTargetContactId;
+
+ /**
+ * Determines whether we display a list item with the label
+ * "Show all contacts" or actually show all contacts
+ */
+ private boolean mJoinModeShowAllContacts;
+
+ /**
+ * The ID of the special item described above.
+ */
+ private static final long JOIN_MODE_SHOW_ALL_CONTACTS_ID = -2;
+
+ private boolean mLoadingJoinSuggestions;
+
+ private JoinContactListAdapter mAdapter;
+
+ @Override
+ protected void resolveIntent(Intent intent) {
+ mMode = MODE_PICK_CONTACT;
+ mTargetContactId = intent.getLongExtra(EXTRA_TARGET_CONTACT_ID, -1);
+ if (mTargetContactId == -1) {
+ Log.e(TAG, "Intent " + intent.getAction() + " is missing required extra: "
+ + EXTRA_TARGET_CONTACT_ID);
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+
+ @Override
+ public void initContentView() {
+ setContentView(R.layout.contacts_list_content_join);
+ TextView blurbView = (TextView)findViewById(R.id.join_contact_blurb);
+
+ String blurb = getString(R.string.blurbJoinContactDataWith,
+ getContactDisplayName(mTargetContactId));
+ blurbView.setText(blurb);
+ mJoinModeShowAllContacts = true;
+ mAdapter = new JoinContactListAdapter(this);
+ setupListView(mAdapter);
+ }
+
+ @Override
+ protected void onListItemClick(int position, long id) {
+ if (id == JOIN_MODE_SHOW_ALL_CONTACTS_ID) {
+ mJoinModeShowAllContacts = false;
+ startQuery();
+ } else {
+ final Uri uri = getSelectedUri(position);
+ returnPickerResult(null, null, uri);
+ }
+ }
+
+ @Override
+ protected Uri getUriToQuery() {
+ return getJoinSuggestionsUri(null);
+ }
+
+ /*
+ * TODO: move to a background thread.
+ */
+ private String getContactDisplayName(long contactId) {
+ String contactName = null;
+ Cursor c = getContentResolver().query(
+ ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
+ new String[] {Contacts.DISPLAY_NAME}, null, null, null);
+ try {
+ if (c != null && c.moveToFirst()) {
+ contactName = c.getString(0);
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+
+ if (contactName == null) {
+ contactName = "";
+ }
+
+ return contactName;
+ }
+
+ private Uri getJoinSuggestionsUri(String filter) {
+ Builder builder = Contacts.CONTENT_URI.buildUpon();
+ builder.appendEncodedPath(String.valueOf(mTargetContactId));
+ builder.appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY);
+ if (!TextUtils.isEmpty(filter)) {
+ builder.appendEncodedPath(Uri.encode(filter));
+ }
+ builder.appendQueryParameter("limit", String.valueOf(MAX_SUGGESTIONS));
+ return builder.build();
+ }
+
+ @Override
+ Cursor doFilter(String filter) {
+ throw new UnsupportedOperationException();
+ }
+
+ private Cursor getShowAllContactsLabelCursor(String[] projection) {
+ MatrixCursor matrixCursor = new MatrixCursor(projection);
+ Object[] row = new Object[projection.length];
+ // The only columns we care about is the id
+ row[SUMMARY_ID_COLUMN_INDEX] = JOIN_MODE_SHOW_ALL_CONTACTS_ID;
+ matrixCursor.addRow(row);
+ return matrixCursor;
+ }
+
+ @Override
+ protected void startQuery(Uri uri, String[] projection) {
+ mLoadingJoinSuggestions = true;
+ startQuery(uri, projection, null, null, null);
+ }
+
+ @Override
+ protected void onQueryComplete(Cursor cursor) {
+ // Whenever we get a suggestions cursor, we need to immediately kick off
+ // another query for the complete list of contacts
+ if (cursor != null && mLoadingJoinSuggestions) {
+ mLoadingJoinSuggestions = false;
+ if (cursor.getCount() > 0) {
+ mAdapter.setSuggestionsCursor(cursor);
+ } else {
+ cursor.close();
+ mAdapter.setSuggestionsCursor(null);
+ }
+
+ if (mAdapter.mSuggestionsCursorCount == 0
+ || !mJoinModeShowAllContacts) {
+ startQuery(getContactFilterUri(getTextFilter()),
+ CONTACTS_SUMMARY_PROJECTION,
+ Contacts._ID + " != " + mTargetContactId
+ + " AND " + ContactsContract.Contacts.IN_VISIBLE_GROUP + "=1", null,
+ getSortOrder(CONTACTS_SUMMARY_PROJECTION));
+ return;
+ }
+
+ cursor = getShowAllContactsLabelCursor(CONTACTS_SUMMARY_PROJECTION);
+ }
+
+ super.onQueryComplete(cursor);
+ }
+
+ private class JoinContactListAdapter extends ContactItemListAdapter {
+ Cursor mSuggestionsCursor;
+ int mSuggestionsCursorCount;
+
+ public JoinContactListAdapter(Context context) {
+ super(context);
+ }
+
+ public void setSuggestionsCursor(Cursor cursor) {
+ if (mSuggestionsCursor != null) {
+ mSuggestionsCursor.close();
+ }
+ mSuggestionsCursor = cursor;
+ mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount();
+ }
+
+ private boolean isShowAllContactsItemPosition(int position) {
+ return mJoinModeShowAllContacts
+ && mSuggestionsCursorCount != 0 && position == mSuggestionsCursorCount + 2;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (!mDataValid) {
+ throw new IllegalStateException(
+ "this should only be called when the cursor is valid");
+ }
+
+ if (isShowAllContactsItemPosition(position)) {
+ return getLayoutInflater().
+ inflate(R.layout.contacts_list_show_all_item, parent, false);
+ }
+
+ // Handle the separator specially
+ int separatorId = getSeparatorId(position);
+ if (separatorId != 0) {
+ TextView view = (TextView) getLayoutInflater().
+ inflate(R.layout.list_separator, parent, false);
+ view.setText(separatorId);
+ return view;
+ }
+
+ boolean showingSuggestion;
+ Cursor cursor;
+ if (mSuggestionsCursorCount != 0 && position < mSuggestionsCursorCount + 2) {
+ showingSuggestion = true;
+ cursor = mSuggestionsCursor;
+ } else {
+ showingSuggestion = false;
+ cursor = mCursor;
+ }
+
+ int realPosition = getRealPosition(position);
+ if (!cursor.moveToPosition(realPosition)) {
+ throw new IllegalStateException("couldn't move cursor to position " + position);
+ }
+
+ boolean newView;
+ View v;
+ if (convertView == null || convertView.getTag() == null) {
+ newView = true;
+ v = newView(mContext, cursor, parent);
+ } else {
+ newView = false;
+ v = convertView;
+ }
+ bindView(v, mContext, cursor);
+ bindSectionHeader(v, realPosition, !showingSuggestion);
+ return v;
+ }
+
+ @Override
+ public void changeCursor(Cursor cursor) {
+ if (cursor == null) {
+ mAdapter.setSuggestionsCursor(null);
+ }
+
+ super.changeCursor(cursor);
+ }
+ @Override
+ public int getItemViewType(int position) {
+ if (isShowAllContactsItemPosition(position)) {
+ return IGNORE_ITEM_VIEW_TYPE;
+ }
+
+ return super.getItemViewType(position);
+ }
+
+ private int getSeparatorId(int position) {
+ if (mSuggestionsCursorCount != 0) {
+ if (position == 0) {
+ return R.string.separatorJoinAggregateSuggestions;
+ } else if (position == mSuggestionsCursorCount + 1) {
+ return R.string.separatorJoinAggregateAll;
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return super.areAllItemsEnabled() && mSuggestionsCursorCount == 0;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ if (position == 0) {
+ return false;
+ }
+
+ if (mSuggestionsCursorCount > 0) {
+ return position != 0 && position != mSuggestionsCursorCount + 1;
+ }
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ if (!mDataValid) {
+ return 0;
+ }
+ int superCount = super.getCount();
+ if (mSuggestionsCursorCount != 0) {
+ // When showing suggestions, we have 2 additional list items: the "Suggestions"
+ // and "All contacts" headers.
+ return mSuggestionsCursorCount + superCount + 2;
+ }
+ return superCount;
+ }
+
+ private int getRealPosition(int pos) {
+ if (mSuggestionsCursorCount != 0) {
+ // When showing suggestions, we have 2 additional list items: the "Suggestions"
+ // and "All contacts" separators.
+ if (pos < mSuggestionsCursorCount + 2) {
+ // We are in the upper partition (Suggestions). Adjusting for the "Suggestions"
+ // separator.
+ return pos - 1;
+ } else {
+ // We are in the lower partition (All contacts). Adjusting for the size
+ // of the upper partition plus the two separators.
+ return pos - mSuggestionsCursorCount - 2;
+ }
+ } else {
+ // No separator, identity map
+ return pos;
+ }
+ }
+
+ @Override
+ public Object getItem(int pos) {
+ if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) {
+ mSuggestionsCursor.moveToPosition(getRealPosition(pos));
+ return mSuggestionsCursor;
+ } else {
+ int realPosition = getRealPosition(pos);
+ if (realPosition < 0) {
+ return null;
+ }
+ return super.getItem(realPosition);
+ }
+ }
+
+ @Override
+ public long getItemId(int pos) {
+ if (mSuggestionsCursorCount != 0 && pos < mSuggestionsCursorCount + 2) {
+ if (mSuggestionsCursor.moveToPosition(pos - 1)) {
+ return mSuggestionsCursor.getLong(mRowIDColumn);
+ } else {
+ return 0;
+ }
+ }
+ int realPosition = getRealPosition(pos);
+ if (realPosition < 0) {
+ return 0;
+ }
+ return super.getItemId(realPosition);
+ }
+ }
+}
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index ead6a4a..c15a40d 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -685,11 +685,8 @@
if (mCursor.moveToFirst()) {
displayName = mCursor.getString(0);
}
- Intent intent = new Intent(ContactsListActivity.JOIN_AGGREGATE);
- intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_ID, freshId);
- if (displayName != null) {
- intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_NAME, displayName);
- }
+ Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT);
+ intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, freshId);
startActivityForResult(intent, REQUEST_JOIN_CONTACT);
}
}
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
new file mode 100644
index 0000000..5dabdc7
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.activities;
+
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+import com.android.contacts.mvcframework.DialogManager;
+import com.android.contacts.views.detail.ContactDetailView;
+import com.android.contacts.views.detail.ContactLoader;
+import com.android.contacts.views.detail.ContactLoader.Result;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class ContactDetailActivity extends Activity implements ContactLoader.Callbacks,
+ DialogManager.DialogShowingViewActivity {
+ private ContactDetailView mDetails;
+ private ContactLoader mLoader;
+ private Uri mUri;
+ private DialogManager mDialogManager;
+
+ private static final String TAG = "ContactDetailActivity";
+
+ private static final int DIALOG_VIEW_DIALOGS_ID1 = 1;
+ private static final int DIALOG_VIEW_DIALOGS_ID2 = 2;
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+
+ setContentView(R.layout.contact_detail);
+
+ mDialogManager = new DialogManager(this, DIALOG_VIEW_DIALOGS_ID1, DIALOG_VIEW_DIALOGS_ID2);
+
+ mDetails = (ContactDetailView) findViewById(R.id.contact_details);
+ mDetails.setCallbacks(new ContactDetailView.DefaultCallbacks(this));
+
+ mUri = getIntent().getData();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ if (mLoader == null) {
+ // Look for a passed along loader and create a new one if it's not there
+ mLoader = (ContactLoader) getLastNonConfigurationInstance();
+ if (mLoader == null) {
+ mLoader = new ContactLoader(this, mUri);
+ }
+ }
+ mLoader.registerCallbacks(this);
+ mLoader.startLoading();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ // Let the loader know we're done with it
+ mLoader.unregisterCallbacks(this);
+
+ // The loader isn't getting passed along to the next instance so ask it to stop loading
+ // TODO: Readd this once we have framework support
+ /*if (!isChangingConfigurations()) {
+ mLoader.stopLoading();
+ }*/
+ }
+
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ // Pass the loader along to the next guy
+ Object result = mLoader;
+ mLoader = null;
+ return result;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (mLoader != null) {
+ mLoader.destroy();
+ }
+ }
+
+ public void onContactLoaded(Result contact) {
+ if (contact == ContactLoader.Result.NOT_FOUND) {
+ // Item has been deleted
+ Log.i(TAG, "No contact found. Closing activity");
+ finish();
+ return;
+ }
+ mDetails.setData(contact);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // TODO: This is too hardwired.
+ if (mDetails.onCreateOptionsMenu(menu, getMenuInflater())) return true;
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ // TODO: This is too hardwired.
+ if (mDetails.onPrepareOptionsMenu(menu)) return true;
+
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // TODO: This is too hardwired.
+ if (mDetails.onOptionsItemSelected(item)) return true;
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ public DialogManager getDialogManager() {
+ return mDialogManager;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id, Bundle args) {
+ return mDialogManager.onCreateDialog(id, args);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ // TODO: This is too hardwired.
+ if (mDetails.onContextItemSelected(item)) return true;
+
+ return super.onContextItemSelected(item);
+ }
+
+ @Override
+ public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
+ boolean globalSearch) {
+ if (globalSearch) {
+ super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+ } else {
+ ContactsSearchManager.startSearch(this, initialQuery);
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // TODO: This is too hardwired.
+ if (mDetails.onKeyDown(keyCode, event)) return true;
+
+ return super.onKeyDown(keyCode, event);
+ }
+}
diff --git a/src/com/android/contacts/mvcframework/CursorLoader.java b/src/com/android/contacts/mvcframework/CursorLoader.java
new file mode 100644
index 0000000..89ae997
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/CursorLoader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.mvcframework;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.AsyncTask;
+
+public abstract class CursorLoader extends Loader<CursorLoader.Callbacks> {
+ private Context mContext;
+ private Cursor mCursor;
+ private ForceLoadContentObserver mObserver;
+ private boolean mClosed;
+
+ public interface Callbacks {
+ public void onCursorLoaded(Cursor cursor);
+ }
+
+ protected Context getContext() {
+ return mContext;
+ }
+
+ final class LoadListTask extends AsyncTask<Void, Void, Cursor> {
+ /* Runs on a worker thread */
+ @Override
+ protected Cursor doInBackground(Void... params) {
+ Cursor cursor = doQueryInBackground();
+ // Ensure the data is loaded
+ if (cursor != null) {
+ cursor.getCount();
+ cursor.registerContentObserver(mObserver);
+ }
+ return cursor;
+ }
+
+ /* Runs on the UI thread */
+ @Override
+ protected void onPostExecute(Cursor cursor) {
+ if (mClosed) {
+ // An async query came in after the call to close()
+ cursor.close();
+ return;
+ }
+ mCursor = cursor;
+ if (mCallbacks != null) {
+ // A listener is register, notify them of the result
+ mCallbacks.onCursorLoaded(cursor);
+ }
+ }
+ }
+
+ public CursorLoader(Context context) {
+ mContext = context.getApplicationContext();
+ mObserver = new ForceLoadContentObserver();
+ }
+
+ /**
+ * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
+ * will be called on the UI thread. If a previous load has been completed and is still valid
+ * the result may be passed to the callbacks immediately.
+ *
+ * Must be called from the UI thread
+ */
+ @Override
+ public void startLoading() {
+ if (mCursor != null) {
+ mCallbacks.onCursorLoaded(mCursor);
+ } else {
+ forceLoad();
+ }
+ }
+
+ /**
+ * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously
+ * loaded data set and load a new one.
+ */
+ @Override
+ public void forceLoad() {
+ new LoadListTask().execute((Void[]) null);
+ }
+
+ /**
+ * Must be called from the UI thread
+ */
+ @Override
+ public void stopLoading() {
+ if (mCursor != null && !mCursor.isClosed()) {
+ mCursor.close();
+ mCursor = null;
+ }
+ }
+
+ @Override
+ public void destroy() {
+ // Close up the cursor
+ stopLoading();
+ // Make sure that any outstanding loads clean themselves up properly
+ mClosed = true;
+ }
+
+ /** Called from a worker thread to execute the desired query */
+ protected abstract Cursor doQueryInBackground();
+}
diff --git a/src/com/android/contacts/mvcframework/DialogManager.java b/src/com/android/contacts/mvcframework/DialogManager.java
new file mode 100644
index 0000000..420c4b2
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/DialogManager.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.mvcframework;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.os.Bundle;
+import android.view.View;
+
+/**
+ * Manages creation and destruction of Dialogs that are to be shown by Views. Unlike how Dialogs
+ * are regularly used, the Dialogs are not recycled but immediately destroyed after dismissal.
+ * To be able to do that, two IDs are required which are used consecutively.
+ * How to use:<ul>
+ * <li>The owning Activity creates on instance of this class, passing itself and two Ids that are
+ * not used by other Dialogs of the Activity.</li>
+ * <li>Views owning Dialogs must implement {@link DialogManager.DialogShowingView}</li>
+ * <li>After creating the Views, configureManagingViews must be called to configure all views
+ * that implement {@link DialogManager.DialogShowingView}</li>
+ * <li>In the implementation of {@link Activity#onCreateDialog}, calls for the
+ * ViewId are forwarded to {@link DialogManager#onCreateDialog(int, Bundle)}</li>
+ * </ul>
+ * To actually show a Dialog, the View uses {@link DialogManager#showDialogInView(View, Bundle)},
+ * passing itself as a first parameter
+ */
+public class DialogManager {
+ private final Activity mActivity;
+ private final int mDialogId1;
+ private final int mDialogId2;
+ private boolean mUseDialogId2 = false;
+ public final static String VIEW_ID_KEY = "view_id";
+
+ /**
+ * Creates a new instance of this class for the given Activity.
+ * @param activity The activity this object is used for
+ * @param dialogId1 The first Id that is reserved for use by child-views
+ * @param dialogId2 The second Id that is reserved for use by child-views
+ */
+ public DialogManager(final Activity activity, final int dialogId1, final int dialogId2) {
+ if (activity == null) throw new IllegalArgumentException("activity must not be null");
+ if (dialogId1 == dialogId2) throw new IllegalArgumentException("Ids must be different");
+ mActivity = activity;
+ mDialogId1 = dialogId1;
+ mDialogId2 = dialogId2;
+ }
+
+ /**
+ * Called by a View to show a dialog. It has to pass itself and a Bundle with extra information.
+ * If the view can show several dialogs, it should distinguish them using an item in the Bundle.
+ * The View needs to have a valid and unique Id. This function modifies the bundle by adding a
+ * new item named {@link DialogManager#VIEW_ID_KEY}
+ */
+ public void showDialogInView(final View view, final Bundle bundle) {
+ final int viewId = view.getId();
+ if (bundle.containsKey(VIEW_ID_KEY)) {
+ throw new IllegalArgumentException("Bundle already contains a " + VIEW_ID_KEY);
+ }
+ if (viewId == View.NO_ID) {
+ throw new IllegalArgumentException("View does not have a proper ViewId");
+ }
+ bundle.putInt(VIEW_ID_KEY, viewId);
+ int dialogId = mUseDialogId2 ? mDialogId2 : mDialogId1;
+ mActivity.showDialog(dialogId, bundle);
+ }
+
+ /**
+ * Callback function called by the Activity to handle View-managed Dialogs.
+ * This function returns null if the id is not one of the two reserved Ids.
+ */
+ public Dialog onCreateDialog(final int id, final Bundle bundle) {
+ if (id == mDialogId1) {
+ mUseDialogId2 = true;
+ } else if (id == mDialogId2) {
+ mUseDialogId2 = false;
+ } else {
+ return null;
+ }
+ if (!bundle.containsKey(VIEW_ID_KEY)) {
+ throw new IllegalArgumentException("Bundle does not contain a ViewId");
+ }
+ final int viewId = bundle.getInt(VIEW_ID_KEY);
+ final View view = mActivity.findViewById(viewId);
+ if (view == null || !(view instanceof DialogShowingView)) {
+ return null;
+ }
+ final Dialog dialog = ((DialogShowingView)view).createDialog(bundle);
+
+ // As we will never re-use this dialog, we can completely kill it here
+ dialog.setOnDismissListener(new OnDismissListener() {
+ public void onDismiss(DialogInterface dialogInterface) {
+ mActivity.removeDialog(id);
+ }
+ });
+ return dialog;
+ }
+
+ /**
+ * Interface to implemented by Views that show Dialogs
+ */
+ public interface DialogShowingView {
+ /**
+ * Callback function to create a Dialog. Notice that the DialogManager overwrites the
+ * OnDismissListener on the returned Dialog, so the View should not use this Listener itself
+ */
+ Dialog createDialog(Bundle bundle);
+ }
+
+ /**
+ * Interface to implemented by Activities that host View-showing dialogs
+ */
+ public interface DialogShowingViewActivity {
+ DialogManager getDialogManager();
+ }
+}
diff --git a/src/com/android/contacts/mvcframework/Loader.java b/src/com/android/contacts/mvcframework/Loader.java
new file mode 100644
index 0000000..456b53a
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/Loader.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.mvcframework;
+
+import android.database.ContentObserver;
+import android.os.Handler;
+
+public abstract class Loader<E> {
+ protected E mCallbacks;
+
+ protected final class ForceLoadContentObserver extends ContentObserver {
+ public ForceLoadContentObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ forceLoad();
+ }
+ }
+
+ /**
+ * Registers a class that will receive callbacks when a load is complete. The callbacks will
+ * be called on the UI thread so it's safe to pass the results to widgets.
+ *
+ * Must be called from the UI thread
+ */
+ public void registerCallbacks(E callbacks) {
+ if (mCallbacks != null) {
+ throw new IllegalStateException("There are already callbacks registered");
+ }
+ mCallbacks = callbacks;
+ }
+
+ /**
+ * Must be called from the UI thread
+ */
+ public void unregisterCallbacks(E callbacks) {
+ if (mCallbacks == null) {
+ throw new IllegalStateException("No callbacks register");
+ }
+ if (mCallbacks != callbacks) {
+ throw new IllegalArgumentException("Attempting to unregister the wrong callbacks");
+ }
+ mCallbacks = null;
+ }
+
+ /**
+ * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
+ * will be called on the UI thread. If a previous load has been completed and is still valid
+ * the result may be passed to the callbacks immediately. The loader will monitor the source of
+ * the data set and may deliver future callbacks if the source changes. Calling
+ * {@link #stopLoading} will stop the delivery of callbacks.
+ *
+ * Must be called from the UI thread
+ */
+ public abstract void startLoading();
+
+ /**
+ * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously
+ * loaded data set and load a new one.
+ */
+ public abstract void forceLoad();
+
+ /**
+ * Stops delivery of updates.
+ */
+ public abstract void stopLoading();
+
+ /**
+ * Must be called from the UI thread
+ */
+ public abstract void destroy();
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index c70cff6..993417d 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -16,9 +16,9 @@
package com.android.contacts.ui;
-import com.android.contacts.ContactsListActivity;
import com.android.contacts.ContactsSearchManager;
import com.android.contacts.ContactsUtils;
+import com.android.contacts.JoinContactActivity;
import com.android.contacts.R;
import com.android.contacts.model.ContactsSource;
import com.android.contacts.model.Editor;
@@ -30,11 +30,11 @@
import com.android.contacts.model.ContactsSource.EditType;
import com.android.contacts.model.Editor.EditorListener;
import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.mvcframework.DialogManager;
import com.android.contacts.ui.widget.BaseContactEditorView;
import com.android.contacts.ui.widget.PhotoEditorView;
import com.android.contacts.util.EmptyService;
import com.android.contacts.util.WeakAsyncTask;
-import com.google.android.collect.Lists;
import android.accounts.Account;
import android.app.Activity;
@@ -53,6 +53,7 @@
import android.content.Intent;
import android.content.OperationApplicationException;
import android.content.ContentProviderOperation.Builder;
+import android.content.DialogInterface.OnDismissListener;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.media.MediaScannerConnection;
@@ -94,7 +95,8 @@
* Activity for editing or inserting a contact.
*/
public final class EditContactActivity extends Activity
- implements View.OnClickListener, Comparator<EntityDelta> {
+ implements View.OnClickListener, Comparator<EntityDelta>,
+ DialogManager.DialogShowingViewActivity {
private static final String TAG = "EditContactActivity";
@@ -127,6 +129,13 @@
private static final int DIALOG_CONFIRM_READONLY_DELETE = 2;
private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3;
private static final int DIALOG_CONFIRM_READONLY_HIDE = 4;
+ private static final int DIALOG_PICK_PHOTO = 5;
+ private static final int DIALOG_SPLIT = 6;
+ private static final int DIALOG_SELECT_ACCOUNT = 7;
+ private static final int DIALOG_VIEW_DIALOGS_ID1 = 8;
+ private static final int DIALOG_VIEW_DIALOGS_ID2 = 9;
+
+ private static final String BUNDLE_SELECT_ACCOUNT_LIST = "account_list";
private static final int ICON_SIZE = 96;
@@ -144,14 +153,13 @@
private static final int STATUS_SAVING = 2;
private int mStatus;
+ private DialogManager mDialogManager;
EntitySet mState;
/** The linear layout holding the ContactEditorViews */
LinearLayout mContent;
- private ArrayList<Dialog> mManagedDialogs = Lists.newArrayList();
-
private ViewIdGenerator mViewIdGenerator;
@Override
@@ -163,6 +171,8 @@
setContentView(R.layout.act_edit);
+ mDialogManager = new DialogManager(this, DIALOG_VIEW_DIALOGS_ID1, DIALOG_VIEW_DIALOGS_ID2);
+
// Build editor and listen for photo requests
mContent = (LinearLayout) findViewById(R.id.editors);
@@ -295,15 +305,6 @@
}
@Override
- protected void onDestroy() {
- super.onDestroy();
-
- for (Dialog dialog : mManagedDialogs) {
- dismissDialog(dialog);
- }
- }
-
- @Override
protected Dialog onCreateDialog(int id, Bundle bundle) {
switch (id) {
case DIALOG_CONFIRM_DELETE:
@@ -341,25 +342,15 @@
.setPositiveButton(android.R.string.ok, new DeleteClickListener())
.setCancelable(false)
.create();
+ case DIALOG_PICK_PHOTO:
+ return createPickPhotoDialog();
+ case DIALOG_SPLIT:
+ return createSplitDialog();
+ case DIALOG_SELECT_ACCOUNT:
+ return createSelectAccountDialog(bundle);
+ default:
+ return mDialogManager.onCreateDialog(id, bundle);
}
- return null;
- }
-
- /**
- * Start managing this {@link Dialog} along with the {@link Activity}.
- */
- private void startManagingDialog(Dialog dialog) {
- synchronized (mManagedDialogs) {
- mManagedDialogs.add(dialog);
- }
- }
-
- /**
- * Show this {@link Dialog} and manage with the {@link Activity}.
- */
- void showAndManageDialog(Dialog dialog) {
- startManagingDialog(dialog);
- dialog.show();
}
/**
@@ -856,8 +847,8 @@
}
mContactIdForJoin = ContentUris.parseId(contactLookupUri);
- Intent intent = new Intent(ContactsListActivity.JOIN_AGGREGATE);
- intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_ID, mContactIdForJoin);
+ Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT);
+ intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, mContactIdForJoin);
startActivityForResult(intent, REQUEST_JOIN_CONTACT);
}
@@ -1021,7 +1012,7 @@
mRawContactIdRequestingPhoto = rawContactId;
- showAndManageDialog(createPickPhotoDialog());
+ showDialog(DIALOG_PICK_PHOTO);
return true;
}
@@ -1036,8 +1027,7 @@
final Context dialogContext = new ContextThemeWrapper(context,
android.R.style.Theme_Light);
- String[] choices;
- choices = new String[2];
+ String[] choices = new String[2];
choices[0] = getString(R.string.take_photo);
choices[1] = getString(R.string.pick_photo);
final ListAdapter adapter = new ArrayAdapter<String>(dialogContext,
@@ -1167,7 +1157,7 @@
private boolean doSplitContactAction() {
if (!hasValidState()) return false;
- showAndManageDialog(createSplitDialog());
+ showDialog(DIALOG_SPLIT);
return true;
}
@@ -1229,6 +1219,14 @@
return; // Don't show a dialog.
}
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArrayList(BUNDLE_SELECT_ACCOUNT_LIST, accounts);
+ showDialog(DIALOG_SELECT_ACCOUNT, bundle);
+ }
+
+ private Dialog createSelectAccountDialog(Bundle bundle) {
+ final ArrayList<Account> accounts = bundle.getParcelableArrayList(
+ BUNDLE_SELECT_ACCOUNT_LIST);
// Wrap our context to inflate list items using correct theme
final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light);
final LayoutInflater dialogInflater =
@@ -1285,7 +1283,13 @@
builder.setTitle(R.string.dialog_new_contact_account);
builder.setSingleChoiceItems(accountAdapter, 0, clickListener);
builder.setOnCancelListener(cancelListener);
- showAndManageDialog(builder.create());
+ final Dialog result = builder.create();
+ result.setOnDismissListener(new OnDismissListener() {
+ public void onDismiss(DialogInterface dialog) {
+ removeDialog(DIALOG_SELECT_ACCOUNT);
+ }
+ });
+ return result;
}
/**
@@ -1409,4 +1413,8 @@
ContactsSearchManager.startSearch(this, initialQuery);
}
}
+
+ public DialogManager getDialogManager() {
+ return mDialogManager;
+ }
}
diff --git a/src/com/android/contacts/ui/widget/GenericEditorView.java b/src/com/android/contacts/ui/widget/GenericEditorView.java
index 24262bb..30ef8c1 100644
--- a/src/com/android/contacts/ui/widget/GenericEditorView.java
+++ b/src/com/android/contacts/ui/widget/GenericEditorView.java
@@ -25,6 +25,8 @@
import com.android.contacts.model.ContactsSource.EditField;
import com.android.contacts.model.ContactsSource.EditType;
import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.mvcframework.DialogManager;
+import com.android.contacts.mvcframework.DialogManager.DialogShowingView;
import com.android.contacts.ui.ViewIdGenerator;
import android.app.AlertDialog;
@@ -32,6 +34,7 @@
import android.content.Context;
import android.content.DialogInterface;
import android.content.Entity;
+import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.telephony.PhoneNumberFormattingTextWatcher;
@@ -58,10 +61,15 @@
* the entry. Uses {@link ValuesDelta} to read any existing
* {@link Entity} values, and to correctly write any changes values.
*/
-public class GenericEditorView extends RelativeLayout implements Editor, View.OnClickListener {
+public class GenericEditorView extends RelativeLayout implements Editor, View.OnClickListener,
+ DialogShowingView {
protected static final int RES_FIELD = R.layout.item_editor_field;
protected static final int RES_LABEL_ITEM = android.R.layout.simple_list_item_1;
+ private static final String DIALOG_ID_KEY = "dialog_id";
+ private static final int DIALOG_ID_LABEL = 1;
+ private static final int DIALOG_ID_CUSTOM = 2;
+
protected LayoutInflater mInflater;
protected static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT
@@ -85,6 +93,7 @@
private EditType mPendingType;
private ViewIdGenerator mViewIdGenerator;
+ private DialogManager mDialogManager = null;
public GenericEditorView(Context context) {
super(context);
@@ -354,7 +363,7 @@
// Only when the custum value input in the next step is correct one.
// this method also set the type value to what the user requested here.
mPendingType = selected;
- createCustomDialog().show();
+ showDialog(DIALOG_ID_CUSTOM);
} else {
// User picked type, and we're sure it's ok to actually write the entry.
mType = selected;
@@ -376,7 +385,7 @@
public void onClick(View v) {
switch (v.getId()) {
case R.id.edit_label: {
- createLabelDialog().show();
+ showDialog(DIALOG_ID_LABEL);
break;
}
case R.id.edit_delete: {
@@ -402,6 +411,26 @@
}
}
+ /* package */
+ void showDialog(int bundleDialogId) {
+ Bundle bundle = new Bundle();
+ bundle.putInt(DIALOG_ID_KEY, bundleDialogId);
+ getDialogManager().showDialogInView(this, bundle);
+ }
+
+ private DialogManager getDialogManager() {
+ if (mDialogManager == null) {
+ Context context = getContext();
+ if (!(context instanceof DialogManager.DialogShowingViewActivity)) {
+ throw new IllegalStateException(
+ "View must be hosted in an Activity that implements " +
+ "DialogManager.DialogShowingViewActivity");
+ }
+ mDialogManager = ((DialogManager.DialogShowingViewActivity)context).getDialogManager();
+ }
+ return mDialogManager;
+ }
+
private static class SavedState extends BaseSavedState {
public boolean mHideOptional;
public int[] mVisibilities;
@@ -469,4 +498,17 @@
mFields.getChildAt(i).setVisibility(ss.mVisibilities[i]);
}
}
+
+ public Dialog createDialog(Bundle bundle) {
+ if (bundle == null) throw new IllegalArgumentException("bundle must not be null");
+ int dialogId = bundle.getInt(DIALOG_ID_KEY);
+ switch (dialogId) {
+ case DIALOG_ID_CUSTOM:
+ return createCustomDialog();
+ case DIALOG_ID_LABEL:
+ return createLabelDialog();
+ default:
+ throw new IllegalArgumentException("Invalid dialogId: " + dialogId);
+ }
+ }
}
diff --git a/src/com/android/contacts/views/detail/ContactDetailView.java b/src/com/android/contacts/views/detail/ContactDetailView.java
new file mode 100644
index 0000000..fd68bd2
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactDetailView.java
@@ -0,0 +1,1038 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.detail;
+
+import com.android.contacts.Collapser;
+import com.android.contacts.ContactEntryAdapter;
+import com.android.contacts.ContactOptionsActivity;
+import com.android.contacts.ContactPresenceIconUtil;
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.R;
+import com.android.contacts.TypePrecedence;
+import com.android.contacts.Collapser.Collapsible;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Sources;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.mvcframework.DialogManager;
+import com.android.contacts.util.Constants;
+import com.android.contacts.util.DataStatus;
+import com.android.contacts.views.detail.ContactLoader.Result;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.widget.ContactHeaderWidget;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Entity;
+import android.content.Intent;
+import android.content.Entity.NamedContentValues;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.ParseException;
+import android.net.Uri;
+import android.net.WebAddress;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnCreateContextMenuListener;
+import android.widget.AdapterView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AdapterView.OnItemClickListener;
+
+import java.util.ArrayList;
+
+public class ContactDetailView extends LinearLayout implements OnCreateContextMenuListener,
+ OnItemClickListener, DialogManager.DialogShowingView {
+ private static final String TAG = "ContactDetailsView";
+ private static final boolean SHOW_SEPARATORS = false;
+
+ private static final String DIALOG_ID_KEY = "dialog_id";
+ private static final int DIALOG_CONFIRM_DELETE = 1;
+ private static final int DIALOG_CONFIRM_READONLY_DELETE = 2;
+ private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3;
+ private static final int DIALOG_CONFIRM_READONLY_HIDE = 4;
+
+ private static final int MENU_ITEM_MAKE_DEFAULT = 3;
+
+ private Result mContactData;
+ private Callbacks mCallbacks;
+ private LayoutInflater mInflater;
+ private ContactHeaderWidget mContactHeaderWidget;
+ private ListView mListView;
+ private boolean mShowSmsLinksForAllPhones;
+ private ViewAdapter mAdapter;
+ private Uri mPrimaryPhoneUri = null;
+ private DialogManager mDialogManager = null;
+
+ private int mReadOnlySourcesCnt;
+ private int mWritableSourcesCnt;
+ private boolean mAllRestricted;
+ private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
+ private int mNumPhoneNumbers = 0;
+
+ /**
+ * The view shown if the detail list is empty.
+ * We set this to the list view when first bind the adapter, so that it won't be shown while
+ * we're loading data.
+ */
+ private View mEmptyView;
+
+ /**
+ * A list of distinct contact IDs included in the current contact.
+ */
+ private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
+ private ArrayList<ViewEntry> mPhoneEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mSmsEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mEmailEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mPostalEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mImEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mNicknameEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mOrganizationEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
+ private ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
+
+ public ContactDetailView(Context context) {
+ super(context);
+ }
+
+ public ContactDetailView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setData(Result data) {
+ mContactData = data;
+
+ mContactHeaderWidget.bindFromContactLookupUri(data.getUri());
+ bindData();
+ }
+
+ private void bindData() {
+
+ // Build up the contact entries
+ buildEntries();
+
+ // Collapse similar data items in select sections.
+ Collapser.collapseList(mPhoneEntries);
+ Collapser.collapseList(mSmsEntries);
+ Collapser.collapseList(mEmailEntries);
+ Collapser.collapseList(mPostalEntries);
+ Collapser.collapseList(mImEntries);
+
+ if (mAdapter == null) {
+ mAdapter = new ViewAdapter(mContext, mSections);
+ mListView.setAdapter(mAdapter);
+ } else {
+ mAdapter.setSections(mSections, SHOW_SEPARATORS);
+ }
+ mListView.setEmptyView(mEmptyView);
+ }
+
+ /**
+ * Build up the entries to display on the screen.
+ */
+ private final void buildEntries() {
+ // Clear out the old entries
+ final int numSections = mSections.size();
+ for (int i = 0; i < numSections; i++) {
+ mSections.get(i).clear();
+ }
+
+ mRawContactIds.clear();
+
+ mReadOnlySourcesCnt = 0;
+ mWritableSourcesCnt = 0;
+ mAllRestricted = true;
+ mPrimaryPhoneUri = null;
+ mNumPhoneNumbers = 0;
+
+ mWritableRawContactIds.clear();
+
+ final Sources sources = Sources.getInstance(mContext);
+
+ // Build up method entries
+ if (mContactData == null) {
+ return;
+ }
+
+ for (Entity entity: mContactData.getEntities()) {
+ final ContentValues entValues = entity.getEntityValues();
+ final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
+ final long rawContactId = entValues.getAsLong(RawContacts._ID);
+
+ // Mark when this contact has any unrestricted components
+ final boolean isRestricted = entValues.getAsInteger(RawContacts.IS_RESTRICTED) != 0;
+ if (!isRestricted) mAllRestricted = false;
+
+ if (!mRawContactIds.contains(rawContactId)) {
+ mRawContactIds.add(rawContactId);
+ }
+ ContactsSource contactsSource = sources.getInflatedSource(accountType,
+ ContactsSource.LEVEL_SUMMARY);
+ if (contactsSource != null && contactsSource.readOnly) {
+ mReadOnlySourcesCnt += 1;
+ } else {
+ mWritableSourcesCnt += 1;
+ mWritableRawContactIds.add(rawContactId);
+ }
+
+
+ for (NamedContentValues subValue : entity.getSubValues()) {
+ final ContentValues entryValues = subValue.values;
+ entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
+
+ final long dataId = entryValues.getAsLong(Data._ID);
+ final String mimeType = entryValues.getAsString(Data.MIMETYPE);
+ if (mimeType == null) continue;
+
+ final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
+ ContactsSource.LEVEL_MIMETYPES);
+ if (kind == null) continue;
+
+ final ViewEntry entry = ViewEntry.fromValues(mContext, mimeType, kind,
+ rawContactId, dataId, entryValues);
+
+ final boolean hasData = !TextUtils.isEmpty(entry.data);
+ final boolean isSuperPrimary = entryValues.getAsInteger(
+ Data.IS_SUPER_PRIMARY) != 0;
+
+ if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build phone entries
+ mNumPhoneNumbers++;
+
+ entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ Uri.fromParts(Constants.SCHEME_TEL, entry.data, null));
+ entry.secondaryIntent = new Intent(Intent.ACTION_SENDTO,
+ Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null));
+
+ // Remember super-primary phone
+ if (isSuperPrimary) mPrimaryPhoneUri = entry.uri;
+
+ entry.isPrimary = isSuperPrimary;
+ mPhoneEntries.add(entry);
+
+ if (entry.type == CommonDataKinds.Phone.TYPE_MOBILE
+ || mShowSmsLinksForAllPhones) {
+ // Add an SMS entry
+ if (kind.iconAltRes > 0) {
+ entry.secondaryActionIcon = kind.iconAltRes;
+ }
+ }
+ } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build email entries
+ entry.intent = new Intent(Intent.ACTION_SENDTO,
+ Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null));
+ entry.isPrimary = isSuperPrimary;
+ mEmailEntries.add(entry);
+
+ // When Email rows have status, create additional Im row
+ final DataStatus status = mContactData.getStatuses().get(entry.id);
+ if (status != null) {
+ final String imMime = Im.CONTENT_ITEM_TYPE;
+ final DataKind imKind = sources.getKindOrFallback(accountType,
+ imMime, mContext, ContactsSource.LEVEL_MIMETYPES);
+ final ViewEntry imEntry = ViewEntry.fromValues(mContext,
+ imMime, imKind, rawContactId, dataId, entryValues);
+ imEntry.intent = ContactsUtils.buildImIntent(entryValues);
+ imEntry.applyStatus(status, false);
+ mImEntries.add(imEntry);
+ }
+ } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build postal entries
+ entry.maxLines = 4;
+ entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
+ mPostalEntries.add(entry);
+ } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build IM entries
+ entry.intent = ContactsUtils.buildImIntent(entryValues);
+ if (TextUtils.isEmpty(entry.label)) {
+ entry.label = mContext.getString(R.string.chat).toLowerCase();
+ }
+
+ // Apply presence and status details when available
+ final DataStatus status = mContactData.getStatuses().get(entry.id);
+ if (status != null) {
+ entry.applyStatus(status, false);
+ }
+ mImEntries.add(entry);
+ } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType) &&
+ (hasData || !TextUtils.isEmpty(entry.label))) {
+ // Build organization entries
+ final boolean isNameRawContact =
+ (mContactData.getNameRawContactId() == rawContactId);
+
+ final boolean duplicatesTitle =
+ isNameRawContact
+ && mContactData.getDisplayNameSource()
+ == DisplayNameSources.ORGANIZATION
+ && (!hasData || TextUtils.isEmpty(entry.label));
+
+ if (!duplicatesTitle) {
+ entry.uri = null;
+
+ if (TextUtils.isEmpty(entry.label)) {
+ entry.label = entry.data;
+ entry.data = "";
+ }
+
+ mOrganizationEntries.add(entry);
+ }
+ } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build nickname entries
+ final boolean isNameRawContact =
+ (mContactData.getNameRawContactId() == rawContactId);
+
+ final boolean duplicatesTitle =
+ isNameRawContact
+ && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME;
+
+ if (!duplicatesTitle) {
+ entry.uri = null;
+ mNicknameEntries.add(entry);
+ }
+ } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build note entries
+ entry.uri = null;
+ entry.maxLines = 100;
+ mOtherEntries.add(entry);
+ } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+ // Build note entries
+ entry.uri = null;
+ entry.maxLines = 10;
+ try {
+ WebAddress webAddress = new WebAddress(entry.data);
+ entry.intent = new Intent(Intent.ACTION_VIEW,
+ Uri.parse(webAddress.toString()));
+ } catch (ParseException e) {
+ Log.e(TAG, "Couldn't parse website: " + entry.data);
+ }
+ mOtherEntries.add(entry);
+ } else {
+ // Handle showing custom rows
+ entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
+
+ // Use social summary when requested by external source
+ final DataStatus status = mContactData.getStatuses().get(entry.id);
+ final boolean hasSocial = kind.actionBodySocial && status != null;
+ if (hasSocial) {
+ entry.applyStatus(status, true);
+ }
+
+ if (hasSocial || hasData) {
+ mOtherEntries.add(entry);
+ }
+ }
+ }
+ }
+ }
+
+ public interface Callbacks {
+ public void onPrimaryClick(ViewEntry entry);
+ public void onSecondaryClick(ViewEntry entry);
+ }
+
+ public static final class DefaultCallbacks implements Callbacks {
+ private Context mContext;
+
+ public DefaultCallbacks(Context context) {
+ mContext = context;
+ }
+
+ public void onPrimaryClick(ViewEntry entry) {
+ Intent intent = entry.intent;
+ if (intent != null) {
+ try {
+ mContext.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "No activity found for intent: " + intent);
+ }
+ }
+ }
+
+ public void onSecondaryClick(ViewEntry entry) {
+ Intent intent = entry.secondaryIntent;
+ if (intent != null) {
+ try {
+ mContext.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "No activity found for intent: " + intent);
+ }
+ }
+ }
+ }
+
+ public void setCallbacks(Callbacks callbacks) {
+ mCallbacks = callbacks;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ Context context = getContext();
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mContactHeaderWidget = (ContactHeaderWidget) findViewById(R.id.contact_header_widget);
+ mContactHeaderWidget.showStar(true);
+ mContactHeaderWidget.setExcludeMimes(new String[] {
+ Contacts.CONTENT_ITEM_TYPE
+ });
+
+ mListView = (ListView) findViewById(android.R.id.list);
+ mListView.setOnCreateContextMenuListener(this);
+ mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+ mListView.setOnItemClickListener(this);
+ // Don't set it to mListView yet. We do so later when we bind the adapter.
+ mEmptyView = findViewById(android.R.id.empty);
+
+ // Build the list of sections. The order they're added to mSections dictates the
+ // order they are displayed in the list.
+ mSections.add(mPhoneEntries);
+ mSections.add(mSmsEntries);
+ mSections.add(mEmailEntries);
+ mSections.add(mImEntries);
+ mSections.add(mPostalEntries);
+ mSections.add(mNicknameEntries);
+ mSections.add(mOrganizationEntries);
+ mSections.add(mGroupEntries);
+ mSections.add(mOtherEntries);
+
+ //TODO Read this value from a preference
+ mShowSmsLinksForAllPhones = true;
+ }
+
+ /* package */ static String buildActionString(DataKind kind, ContentValues values,
+ boolean lowerCase, Context context) {
+ if (kind.actionHeader == null) {
+ return null;
+ }
+ CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values);
+ if (actionHeader == null) {
+ return null;
+ }
+ return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
+ }
+
+ /* package */ static String buildDataString(DataKind kind, ContentValues values,
+ Context context) {
+ if (kind.actionBody == null) {
+ return null;
+ }
+ CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
+ return actionBody == null ? null : actionBody.toString();
+ }
+
+ /**
+ * A basic structure with the data for a contact entry in the list.
+ */
+ static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
+ public Context context = null;
+ public String resPackageName = null;
+ public int actionIcon = -1;
+ public boolean isPrimary = false;
+ public int secondaryActionIcon = -1;
+ public Intent intent;
+ public Intent secondaryIntent = null;
+ public int maxLabelLines = 1;
+ public ArrayList<Long> ids = new ArrayList<Long>();
+ public int collapseCount = 0;
+
+ public int presence = -1;
+
+ public CharSequence footerLine = null;
+
+ private ViewEntry() {
+ }
+
+ /**
+ * Build new {@link ViewEntry} and populate from the given values.
+ */
+ public static ViewEntry fromValues(Context context, String mimeType, DataKind kind,
+ long rawContactId, long dataId, ContentValues values) {
+ final ViewEntry entry = new ViewEntry();
+ entry.context = context;
+ entry.contactId = rawContactId;
+ entry.id = dataId;
+ entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
+ entry.mimetype = mimeType;
+ entry.label = buildActionString(kind, values, false, context);
+ entry.data = buildDataString(kind, values, context);
+
+ if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) {
+ entry.type = values.getAsInteger(kind.typeColumn);
+ }
+ if (kind.iconRes > 0) {
+ entry.resPackageName = kind.resPackageName;
+ entry.actionIcon = kind.iconRes;
+ }
+
+ return entry;
+ }
+
+ /**
+ * Apply given {@link DataStatus} values over this {@link ViewEntry}
+ *
+ * @param fillData When true, the given status replaces {@link #data}
+ * and {@link #footerLine}. Otherwise only {@link #presence}
+ * is updated.
+ */
+ public ViewEntry applyStatus(DataStatus status, boolean fillData) {
+ presence = status.getPresence();
+ if (fillData && status.isValid()) {
+ this.data = status.getStatus().toString();
+ this.footerLine = status.getTimestampLabel(context);
+ }
+
+ return this;
+ }
+
+ public boolean collapseWith(ViewEntry entry) {
+ // assert equal collapse keys
+ if (!shouldCollapseWith(entry)) {
+ return false;
+ }
+
+ // Choose the label associated with the highest type precedence.
+ if (TypePrecedence.getTypePrecedence(mimetype, type)
+ > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) {
+ type = entry.type;
+ label = entry.label;
+ }
+
+ // Choose the max of the maxLines and maxLabelLines values.
+ maxLines = Math.max(maxLines, entry.maxLines);
+ maxLabelLines = Math.max(maxLabelLines, entry.maxLabelLines);
+
+ // Choose the presence with the highest precedence.
+ if (StatusUpdates.getPresencePrecedence(presence)
+ < StatusUpdates.getPresencePrecedence(entry.presence)) {
+ presence = entry.presence;
+ }
+
+ // If any of the collapsed entries are primary make the whole thing primary.
+ isPrimary = entry.isPrimary ? true : isPrimary;
+
+ // uri, and contactdId, shouldn't make a difference. Just keep the original.
+
+ // Keep track of all the ids that have been collapsed with this one.
+ ids.add(entry.id);
+ collapseCount++;
+ return true;
+ }
+
+ public boolean shouldCollapseWith(ViewEntry entry) {
+ if (entry == null) {
+ return false;
+ }
+
+ if (!ContactsUtils.shouldCollapse(context, mimetype, data, entry.mimetype,
+ entry.data)) {
+ return false;
+ }
+
+ if (!TextUtils.equals(mimetype, entry.mimetype)
+ || !ContactsUtils.areIntentActionEqual(intent, entry.intent)
+ || !ContactsUtils.areIntentActionEqual(secondaryIntent, entry.secondaryIntent)
+ || actionIcon != entry.actionIcon) {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ /** Cache of the children views of a row */
+ private static class ViewCache {
+ public TextView label;
+ public TextView data;
+ public TextView footer;
+ public ImageView actionIcon;
+ public ImageView presenceIcon;
+ public ImageView primaryIcon;
+ public ImageView secondaryActionButton;
+ public View secondaryActionDivider;
+
+ // Need to keep track of this too
+ public ViewEntry entry;
+ }
+
+ final class ViewAdapter extends ContactEntryAdapter<ViewEntry> implements OnClickListener {
+
+ ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
+ super(context, sections, SHOW_SEPARATORS);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final ViewEntry entry = getEntry(mSections, position, false);
+ final View v;
+ final ViewCache views;
+
+ // Check to see if we can reuse convertView
+ if (convertView != null) {
+ v = convertView;
+ views = (ViewCache) v.getTag();
+ } else {
+ // Create a new view if needed
+ v = mInflater.inflate(R.layout.list_item_text_icons, parent, false);
+
+ // Cache the children
+ views = new ViewCache();
+ views.label = (TextView) v.findViewById(android.R.id.text1);
+ views.data = (TextView) v.findViewById(android.R.id.text2);
+ views.footer = (TextView) v.findViewById(R.id.footer);
+ views.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
+ views.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon);
+ views.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
+ views.secondaryActionButton = (ImageView) v.findViewById(
+ R.id.secondary_action_button);
+ views.secondaryActionButton.setOnClickListener(this);
+ views.secondaryActionDivider = v.findViewById(R.id.divider);
+ v.setTag(views);
+ }
+
+ // Update the entry in the view cache
+ views.entry = entry;
+
+ // Bind the data to the view
+ bindView(v, entry);
+ return v;
+ }
+
+ @Override
+ protected View newView(int position, ViewGroup parent) {
+ // getView() handles this
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected void bindView(View view, ViewEntry entry) {
+ final Resources resources = mContext.getResources();
+ ViewCache views = (ViewCache) view.getTag();
+
+ // Set the label
+ TextView label = views.label;
+ setMaxLines(label, entry.maxLabelLines);
+ label.setText(entry.label);
+
+ // Set the data
+ TextView data = views.data;
+ if (data != null) {
+ if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
+ || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
+ data.setText(PhoneNumberUtils.formatNumber(entry.data));
+ } else {
+ data.setText(entry.data);
+ }
+ setMaxLines(data, entry.maxLines);
+ }
+
+ // Set the footer
+ if (!TextUtils.isEmpty(entry.footerLine)) {
+ views.footer.setText(entry.footerLine);
+ views.footer.setVisibility(View.VISIBLE);
+ } else {
+ views.footer.setVisibility(View.GONE);
+ }
+
+ // Set the primary icon
+ views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
+
+ // Set the action icon
+ ImageView action = views.actionIcon;
+ if (entry.actionIcon != -1) {
+ Drawable actionIcon;
+ if (entry.resPackageName != null) {
+ // Load external resources through PackageManager
+ actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName,
+ entry.actionIcon, null);
+ } else {
+ actionIcon = resources.getDrawable(entry.actionIcon);
+ }
+ action.setImageDrawable(actionIcon);
+ action.setVisibility(View.VISIBLE);
+ } else {
+ // Things should still line up as if there was an icon, so make it invisible
+ action.setVisibility(View.INVISIBLE);
+ }
+
+ // Set the presence icon
+ Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
+ mContext, entry.presence);
+ ImageView presenceIconView = views.presenceIcon;
+ if (presenceIcon != null) {
+ presenceIconView.setImageDrawable(presenceIcon);
+ presenceIconView.setVisibility(View.VISIBLE);
+ } else {
+ presenceIconView.setVisibility(View.GONE);
+ }
+
+ // Set the secondary action button
+ ImageView secondaryActionView = views.secondaryActionButton;
+ Drawable secondaryActionIcon = null;
+ if (entry.secondaryActionIcon != -1) {
+ secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
+ }
+ if (entry.secondaryIntent != null && secondaryActionIcon != null) {
+ secondaryActionView.setImageDrawable(secondaryActionIcon);
+ secondaryActionView.setTag(entry.secondaryIntent);
+ secondaryActionView.setVisibility(View.VISIBLE);
+ views.secondaryActionDivider.setVisibility(View.VISIBLE);
+ } else {
+ secondaryActionView.setVisibility(View.GONE);
+ views.secondaryActionDivider.setVisibility(View.GONE);
+ }
+ }
+
+ private void setMaxLines(TextView textView, int maxLines) {
+ if (maxLines == 1) {
+ textView.setSingleLine(true);
+ textView.setEllipsize(TextUtils.TruncateAt.END);
+ } else {
+ textView.setSingleLine(false);
+ textView.setMaxLines(maxLines);
+ textView.setEllipsize(null);
+ }
+ }
+
+ public void onClick(View v) {
+ Intent intent = (Intent) v.getTag();
+ mContext.startActivity(intent);
+ }
+ }
+
+ public boolean onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
+ inflater.inflate(R.menu.view, menu);
+ return true;
+ }
+
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ // Only allow edit when we have at least one raw_contact id
+ final boolean hasRawContact = (mRawContactIds.size() > 0);
+ menu.findItem(R.id.menu_edit).setEnabled(hasRawContact);
+
+ // Only allow share when unrestricted contacts available
+ menu.findItem(R.id.menu_share).setEnabled(!mAllRestricted);
+
+ return true;
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_edit: {
+ if (mRawContactIds.size() > 0) {
+ long rawContactIdToEdit = mRawContactIds.get(0);
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+ rawContactIdToEdit);
+ mContext.startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
+ return true;
+ } else {
+ // There is no rawContact to edit.
+ return false;
+ }
+ }
+ case R.id.menu_delete: {
+ showDeleteConfirmationDialog();
+ return true;
+ }
+ case R.id.menu_options: {
+ final Intent intent = new Intent(mContext, ContactOptionsActivity.class);
+ intent.setData(mContactData.getLookupUri());
+ mContext.startActivity(intent);
+ return true;
+ }
+ case R.id.menu_share: {
+ if (mAllRestricted) return false;
+
+ final String lookupKey = mContactData.getLookupKey();
+ final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
+
+ final Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType(Contacts.CONTENT_VCARD_TYPE);
+ intent.putExtra(Intent.EXTRA_STREAM, shareUri);
+
+ // Launch chooser to share contact via
+ final CharSequence chooseTitle = mContext.getText(R.string.share_via);
+ final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
+
+ try {
+ mContext.startActivity(chooseIntent);
+ } catch (ActivityNotFoundException ex) {
+ Toast.makeText(mContext, R.string.share_error, Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void showDeleteConfirmationDialog() {
+ if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
+ showDialog(DIALOG_CONFIRM_READONLY_DELETE);
+ } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
+ showDialog(DIALOG_CONFIRM_READONLY_HIDE);
+ } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
+ showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE);
+ } else {
+ showDialog(DIALOG_CONFIRM_DELETE);
+ }
+ }
+
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ AdapterView.AdapterContextMenuInfo info;
+ try {
+ info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ } catch (ClassCastException e) {
+ Log.e(TAG, "bad menuInfo", e);
+ return;
+ }
+
+ // This can be null sometimes, don't crash...
+ if (info == null) {
+ Log.e(TAG, "bad menuInfo");
+ return;
+ }
+
+ ViewEntry entry = ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
+ menu.setHeaderTitle(R.string.contactOptionsTitle);
+ if (entry.mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
+ menu.add(0, 0, 0, R.string.menu_call).setIntent(entry.intent);
+ menu.add(0, 0, 0, R.string.menu_sendSMS).setIntent(entry.secondaryIntent);
+ if (!entry.isPrimary) {
+ menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultNumber);
+ }
+ } else if (entry.mimetype.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
+ menu.add(0, 0, 0, R.string.menu_sendEmail).setIntent(entry.intent);
+ if (!entry.isPrimary) {
+ menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultEmail);
+ }
+ } else if (entry.mimetype.equals(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)) {
+ menu.add(0, 0, 0, R.string.menu_viewAddress).setIntent(entry.intent);
+ }
+ }
+
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
+ if (entry == null) {
+ signalError();
+ return;
+ }
+ Intent intent = entry.intent;
+ if (intent == null) {
+ signalError();
+ return;
+ }
+ try {
+ mContext.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "No activity found for intent: " + intent);
+ signalError();
+ }
+ }
+
+ /**
+ * Signal an error to the user via a beep, or some other method.
+ */
+ private void signalError() {
+ Log.w(TAG, "Should warn the user but we can't because we do not have sonification APIs");
+ }
+
+ private final DialogInterface.OnClickListener mDeleteListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mContext.getContentResolver().delete(mContactData.getLookupUri(), null, null);
+ }
+ };
+
+ public Dialog createDialog(Bundle bundle) {
+ if (bundle == null) throw new IllegalArgumentException("bundle must not be null");
+ int dialogId = bundle.getInt(DIALOG_ID_KEY);
+ switch (dialogId) {
+ case DIALOG_CONFIRM_DELETE:
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.deleteConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, mDeleteListener)
+ .setCancelable(false)
+ .create();
+ case DIALOG_CONFIRM_READONLY_DELETE:
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.readOnlyContactDeleteConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, mDeleteListener)
+ .setCancelable(false)
+ .create();
+ case DIALOG_CONFIRM_MULTIPLE_DELETE:
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.multipleContactDeleteConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, mDeleteListener)
+ .setCancelable(false)
+ .create();
+ case DIALOG_CONFIRM_READONLY_HIDE: {
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.readOnlyContactWarning)
+ .setPositiveButton(android.R.string.ok, mDeleteListener)
+ .create();
+ }
+ default:
+ throw new IllegalArgumentException("Invalid dialogId: " + dialogId);
+ }
+ }
+
+ /* package */ void showDialog(int bundleDialogId) {
+ Bundle bundle = new Bundle();
+ bundle.putInt(DIALOG_ID_KEY, bundleDialogId);
+ getDialogManager().showDialogInView(this, bundle);
+ }
+
+ private DialogManager getDialogManager() {
+ if (mDialogManager == null) {
+ Context context = getContext();
+ if (!(context instanceof DialogManager.DialogShowingViewActivity)) {
+ throw new IllegalStateException(
+ "View must be hosted in an Activity that implements " +
+ "DialogManager.DialogShowingViewActivity");
+ }
+ mDialogManager = ((DialogManager.DialogShowingViewActivity)context).getDialogManager();
+ }
+ return mDialogManager;
+ }
+
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case MENU_ITEM_MAKE_DEFAULT: {
+ if (makeItemDefault(item)) {
+ return true;
+ }
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean makeItemDefault(MenuItem item) {
+ ViewEntry entry = getViewEntryForMenuItem(item);
+ if (entry == null) {
+ return false;
+ }
+
+ // Update the primary values in the data record.
+ ContentValues values = new ContentValues(1);
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+
+ mContext.getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, entry.id),
+ values, null, null);
+ return true;
+ }
+
+ private ViewEntry getViewEntryForMenuItem(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info;
+ try {
+ info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ } catch (ClassCastException e) {
+ Log.e(TAG, "bad menuInfo", e);
+ return null;
+ }
+
+ return ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CALL: {
+ try {
+ ITelephony phone = ITelephony.Stub.asInterface(
+ ServiceManager.checkService("phone"));
+ if (phone != null && !phone.isIdle()) {
+ // Skip out and let the key be handled at a higher level
+ break;
+ }
+ } catch (RemoteException re) {
+ // Fall through and try to call the contact
+ }
+
+ int index = mListView.getSelectedItemPosition();
+ if (index != -1) {
+ final ViewEntry entry = ViewAdapter.getEntry(mSections, index, SHOW_SEPARATORS);
+ if (entry != null &&
+ entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) {
+ mContext.startActivity(entry.intent);
+ return true;
+ }
+ } else if (mPrimaryPhoneUri != null) {
+ // There isn't anything selected, call the default number
+ final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ mPrimaryPhoneUri);
+ mContext.startActivity(intent);
+ return true;
+ }
+ return false;
+ }
+
+ case KeyEvent.KEYCODE_DEL: {
+ showDeleteConfirmationDialog();
+ return true;
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+}
diff --git a/src/com/android/contacts/views/detail/ContactLoader.java b/src/com/android/contacts/views/detail/ContactLoader.java
new file mode 100644
index 0000000..38e7a5d
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactLoader.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.detail;
+
+import com.android.contacts.mvcframework.Loader;
+import com.android.contacts.util.DataStatus;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.provider.ContactsContract.StatusUpdates;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Loads a single Contact and all it constituent RawContacts.
+ */
+public class ContactLoader extends Loader<ContactLoader.Callbacks> {
+ private Context mContext;
+ private Uri mLookupUri;
+ private Result mContact;
+ private ForceLoadContentObserver mObserver;
+ private boolean mDestroyed;
+
+ private static final String TAG = "ContactLoader";
+
+ public interface Callbacks {
+ public void onContactLoaded(Result contact);
+ }
+
+ /**
+ * The result of a load operation. Contains all data necessary to display the contact.
+ */
+ public static final class Result {
+ /**
+ * Singleton instance that represents "No Contact Found"
+ */
+ public static final Result NOT_FOUND = new Result();
+
+ private final Uri mLookupUri;
+ private final String mLookupKey;
+ private final Uri mUri;
+ private final long mId;
+ private final ArrayList<Entity> mEntities;
+ private final HashMap<Long, DataStatus> mStatuses;
+ private final long mNameRawContactId;
+ private final int mDisplayNameSource;
+
+ /**
+ * Constructor for case "no contact found". This must only be used for the
+ * final {@link Result#NOT_FOUND} singleton
+ */
+ private Result() {
+ mLookupUri = null;
+ mLookupKey = null;
+ mUri = null;
+ mId = -1;
+ mEntities = null;
+ mStatuses = null;
+ mNameRawContactId = -1;
+ mDisplayNameSource = DisplayNameSources.UNDEFINED;
+ }
+
+ /**
+ * Constructor to call when contact was found
+ */
+ private Result(Uri lookupUri, String lookupKey, Uri uri, long id, long nameRawContactId,
+ int displayNameSource) {
+ mLookupUri = lookupUri;
+ mLookupKey = lookupKey;
+ mUri = uri;
+ mId = id;
+ mEntities = new ArrayList<Entity>();
+ mStatuses = new HashMap<Long, DataStatus>();
+ mNameRawContactId = nameRawContactId;
+ mDisplayNameSource = displayNameSource;
+ }
+
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+ public String getLookupKey() {
+ return mLookupKey;
+ }
+ public Uri getUri() {
+ return mUri;
+ }
+ public long getId() {
+ return mId;
+ }
+ public ArrayList<Entity> getEntities() {
+ return mEntities;
+ }
+ public HashMap<Long, DataStatus> getStatuses() {
+ return mStatuses;
+ }
+ public long getNameRawContactId() {
+ return mNameRawContactId;
+ }
+ public int getDisplayNameSource() {
+ return mDisplayNameSource;
+ }
+ }
+
+ interface StatusQuery {
+ final String[] PROJECTION = new String[] {
+ Data._ID, Data.STATUS, Data.STATUS_RES_PACKAGE, Data.STATUS_ICON,
+ Data.STATUS_LABEL, Data.STATUS_TIMESTAMP, Data.PRESENCE,
+ };
+
+ final int _ID = 0;
+ }
+
+ final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+ @Override
+ protected Result doInBackground(Void... args) {
+ final ContentResolver resolver = mContext.getContentResolver();
+ Result result = loadContactHeaderData(resolver, mLookupUri);
+ if (result == Result.NOT_FOUND) {
+ // No record found. Try to lookup up a new record with the same lookupKey.
+ // We might have went through a sync where Ids changed
+ final Uri freshLookupUri = Contacts.getLookupUri(resolver, mLookupUri);
+ result = loadContactHeaderData(resolver, freshLookupUri);
+ if (result == Result.NOT_FOUND) {
+ // Still not found. We now believe this contact really does not exist
+ Log.e(TAG, "invalid contact uri: " + mLookupUri);
+ return Result.NOT_FOUND;
+ }
+ }
+
+ // These queries could be run in parallel (we did this until froyo). But unless
+ // we actually have two database connections there is no performance gain
+ loadSocial(resolver, result);
+ loadRawContacts(resolver, result);
+
+ return result;
+ }
+
+ /**
+ * Tries to lookup a contact using both Id and lookup key of the given Uri. Returns a
+ * valid Result instance if successful or {@link Result#NOT_FOUND} if empty
+ */
+ private Result loadContactHeaderData(final ContentResolver resolver,
+ final Uri lookupUri) {
+ if (resolver == null) throw new IllegalArgumentException("resolver must not be null");
+ if (lookupUri == null) {
+ // This can happen if the row was removed
+ return Result.NOT_FOUND;
+ }
+
+ final List<String> segments = lookupUri.getPathSegments();
+ if (segments.size() != 4) {
+ // Does not contain an Id. Return to caller so that a lookup is performed
+ Log.w(TAG, "Uri does not contain an Id, so we return to the caller who should " +
+ "perform a lookup to get a proper uri. Value: " + lookupUri);
+ return Result.NOT_FOUND;
+ }
+
+ final long uriContactId = Long.parseLong(segments.get(3));
+ final String uriLookupKey = Uri.encode(segments.get(2));
+ final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, uriContactId);
+ final Uri dataUri = Uri.withAppendedPath(contactUri, Contacts.Data.CONTENT_DIRECTORY);
+
+ final Cursor cursor = resolver.query(dataUri,
+ new String[] {
+ Contacts.NAME_RAW_CONTACT_ID,
+ Contacts.DISPLAY_NAME_SOURCE,
+ Contacts.LOOKUP_KEY
+ }, null, null, null);
+ if (cursor == null) {
+ Log.e(TAG, "No cursor returned in trySetupContactHeader/query");
+ return null;
+ }
+ try {
+ if (!cursor.moveToFirst()) {
+ Log.w(TAG, "Cursor returned by trySetupContactHeader/query is empty. " +
+ "ContactId must have changed or item has been removed");
+ return Result.NOT_FOUND;
+ }
+ String lookupKey =
+ cursor.getString(cursor.getColumnIndex(Contacts.LOOKUP_KEY));
+ if (!lookupKey.equals(uriLookupKey)) {
+ // ID and lookup key do not match
+ Log.w(TAG, "Contact with Id=" + uriContactId + " has a wrong lookupKey ("
+ + lookupKey + " instead of the expected " + uriLookupKey + ")");
+ return Result.NOT_FOUND;
+ }
+
+ long nameRawContactId = cursor.getLong(cursor.getColumnIndex(
+ Contacts.NAME_RAW_CONTACT_ID));
+ int displayNameSource = cursor.getInt(cursor.getColumnIndex(
+ Contacts.DISPLAY_NAME_SOURCE));
+
+ return new Result(lookupUri, lookupKey, contactUri, uriContactId, nameRawContactId,
+ displayNameSource);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Loads the social rows into the result structure. Expects the statuses in the
+ * result structure to be empty
+ */
+ private void loadSocial(final ContentResolver resolver, final Result result) {
+ if (result == null) throw new IllegalArgumentException("result must not be null");
+ if (resolver == null) throw new IllegalArgumentException("resolver must not be null");
+ if (result == Result.NOT_FOUND) {
+ throw new IllegalArgumentException("result must not be NOT_FOUND");
+ }
+
+ final Uri dataUri = Uri.withAppendedPath(result.getUri(),
+ Contacts.Data.CONTENT_DIRECTORY);
+ final Cursor cursor = resolver.query(dataUri, StatusQuery.PROJECTION,
+ StatusUpdates.PRESENCE + " IS NOT NULL OR " + StatusUpdates.STATUS +
+ " IS NOT NULL", null, null);
+
+ if (cursor == null) {
+ Log.e(TAG, "Social cursor is null but it shouldn't be");
+ return;
+ }
+
+ try {
+ HashMap<Long, DataStatus> statuses = result.getStatuses();
+
+ // Walk found statuses, creating internal row for each
+ while (cursor.moveToNext()) {
+ final DataStatus status = new DataStatus(cursor);
+ final long dataId = cursor.getLong(StatusQuery._ID);
+ statuses.put(dataId, status);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Loads the raw row contact rows into the result structure. Expects the entities in the
+ * result structure to be empty
+ */
+ private void loadRawContacts(final ContentResolver resolver, final Result result) {
+ if (result == null) throw new IllegalArgumentException("result must not be null");
+ if (resolver == null) throw new IllegalArgumentException("resolver must not be null");
+ if (result == Result.NOT_FOUND) {
+ throw new IllegalArgumentException("result must not be NOT_FOUND");
+ }
+
+ // Read the constituent raw contacts
+ final Cursor cursor = resolver.query(RawContactsEntity.CONTENT_URI, null,
+ RawContacts.CONTACT_ID + "=?", new String[] {
+ String.valueOf(result.mId)
+ }, null);
+ if (cursor == null) {
+ Log.e(TAG, "Raw contacts cursor is null but it shouldn't be");
+ return;
+ }
+
+ try {
+ ArrayList<Entity> entities = result.getEntities();
+ entities.ensureCapacity(cursor.getCount());
+ EntityIterator iterator = RawContacts.newEntityIterator(cursor);
+ try {
+ while (iterator.hasNext()) {
+ Entity entity = iterator.next();
+ entities.add(entity);
+ }
+ } finally {
+ iterator.close();
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Result result) {
+ // The creator isn't interested in any furether updates
+ if (mDestroyed) {
+ return;
+ }
+
+ mContact = result;
+ if (result != null) {
+ if (mObserver == null) {
+ mObserver = new ForceLoadContentObserver();
+ }
+ Log.i(TAG, "Registering content observer for " + mLookupUri);
+ mContext.getContentResolver().registerContentObserver(mLookupUri, true, mObserver);
+ mCallbacks.onContactLoaded(result);
+ }
+ }
+ }
+
+ public ContactLoader(Context context, Uri lookupUri) {
+ mContext = context.getApplicationContext();
+ mLookupUri = lookupUri;
+ }
+
+ @Override
+ public void startLoading() {
+ if (mContact != null) {
+ mCallbacks.onContactLoaded(mContact);
+ } else {
+ forceLoad();
+ }
+ }
+
+ @Override
+ public void forceLoad() {
+ new LoadContactTask().execute((Void[])null);
+ }
+
+ @Override
+ public void stopLoading() {
+ mContact = null;
+ if (mObserver != null) {
+ mContext.getContentResolver().unregisterContentObserver(mObserver);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ mContact = null;
+ mDestroyed = true;
+ }
+}