am 639e96ee: Fixing breakage in legacy contact pickers.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 04d0cf4..d920701 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">
@@ -458,6 +463,10 @@
</intent-filter>
</activity>
+ <service
+ android:name=".ImportVCardService"
+ android:exported="false" />
+
<activity android:name=".ExportVCardActivity"
android:theme="@style/BackgroundOnly" />
</application>
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/layout/status_bar_ongoing_event_progress_bar.xml b/res/layout/status_bar_ongoing_event_progress_bar.xml
new file mode 100644
index 0000000..d1bce1a
--- /dev/null
+++ b/res/layout/status_bar_ongoing_event_progress_bar.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright 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.
+ /
+TODO: This is copied from DownloadProvider, and looks similar to Bluetooth's.
+ It might be better to have this in framework.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="@android:drawable/status_bar_item_app_background"
+ >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ >
+
+ <LinearLayout
+ android:layout_width="40dp"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingTop="8dp"
+ android:focusable="true"
+ android:clickable="true"
+ >
+ <com.android.server.status.AnimatedImageView
+ android:id="@+id/appIcon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:src="@android:drawable/sym_def_app_icon"
+ />
+ <TextView android:id="@+id/progress_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="#ff000000"
+ android:singleLine="true"
+ android:textSize="14sp"
+ android:layout_gravity="center_horizontal"
+ />
+ </LinearLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:focusable="true"
+ android:clickable="true"
+ >
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:focusable="true"
+ android:clickable="true"
+ android:layout_alignParentTop="true"
+ android:paddingTop="10dp"
+ >
+ <TextView android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textSize="18sp"
+ android:textColor="#ff000000"
+ android:paddingLeft="2dp"
+ />
+ <TextView android:id="@+id/description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="#ff000000"
+ android:singleLine="true"
+ android:textSize="14sp"
+ android:paddingLeft="5dp"
+ />
+ </LinearLayout>
+ <ProgressBar android:id="@+id/progress_bar"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:paddingBottom="8dp"
+ android:paddingRight="25dp"
+ />
+ </RelativeLayout>
+ </LinearLayout>
+
+ <com.android.server.status.AnimatedImageView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:src="@android:drawable/divider_horizontal_bright"
+ />
+
+</LinearLayout>
+
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/ids.xml b/res/values/ids.xml
index ceb10f8..692c413 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -37,7 +37,7 @@
<item type="id" name="dialog_multiple_contact_delete_confirmation"/>
<item type="id" name="dialog_readonly_contact_delete_confirmation"/>
- <!-- For ExportVCard -->
+ <!-- For ExportVCardActivity -->
<item type="id" name="dialog_export_confirmation"/>
<item type="id" name="dialog_exporting_vcard"/>
<item type="id" name="dialog_fail_to_export_with_reason"/>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 041440a..168880d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -658,7 +658,7 @@
<!-- Dialog message shown when SDcard does not exist -->
<string name="no_sdcard_message">No SD card detected</string>
- <!-- Dialog title shown when searching VCard data from SD Card -->
+ <!-- Dialog title shown when searching vCard data from SD Card -->
<string name="searching_vcard_title">Searching for vCard</string>
<!-- Action string for selecting SIM for importing contacts -->
@@ -688,13 +688,13 @@
than one vCard files available in the system. -->
<string name="import_all_vcard_string">Import all vCard files</string>
- <!-- Dialog message shown when searching VCard data from SD Card -->
+ <!-- Dialog message shown when searching vCard data from SD Card -->
<string name="searching_vcard_message">Searching for vCard data on SD card</string>
- <!-- Dialog title shown when scanning VCard data failed. -->
+ <!-- Dialog title shown when scanning vCard data failed. -->
<string name="scanning_sdcard_failed_title">Scanning SD card failed</string>
- <!-- Dialog message shown when searching VCard data failed.
+ <!-- Dialog message shown when searching vCard data failed.
An exact reason for the failure should -->
<string name="scanning_sdcard_failed_message">Scanning SD card failed (Reason: \"<xliff:g id="fail_reason">%s</xliff:g>\")</string>
@@ -724,28 +724,35 @@
<!-- The failed reason which should not be shown but it may in some buggy condition. -->
<string name="fail_reason_unknown">Unknown error</string>
- <!-- Dialog title shown when a user is asked to select VCard file -->
+ <!-- Dialog title shown when a user is asked to select vCard file -->
<string name="select_vcard_title">Select vCard file</string>
- <!-- The message shown while reading a vCard file/entry. The first argument is like
- "Reading VCard" or "Reading VCard files" and the second is the display name of the current
- data being parsed -->
- <string name="progress_shower_message"><xliff:g id="action" example="Reading VCard">%s</xliff:g>\n<xliff:g id="filename" example="foo.vcf">%s</xliff:g></string>
+ <!-- The message shown while reading vCard(s).
+ First argument is current index of contacts to be imported.
+ Second argument is the total number of contacts.
+ Third argument is the Uri which is being read. -->
+ <string name="progress_notifier_message"><xliff:g id="current_number">%s</xliff:g>/<xliff:g id="total_number">%s</xliff:g>: <xliff:g id="filename" example="foo.vcf">%s</xliff:g></string>
- <!-- Dialog title shown when reading VCard data -->
- <string name="reading_vcard_title">Reading vCard</string>
+ <!-- Dialog title shown when reading vCard data -->
+ <string name="reading_vcard_title">Reading vCard(s)</string>
- <!-- Dialog message shown when reading a VCard file -->
- <string name="reading_vcard_message">Reading vCard file(s)</string>
+ <!-- Dialog title shown when reading vCard data failed -->
+ <string name="reading_vcard_failed_title">Failed to Read vCard data</string>
- <!-- Dialog title shown when reading VCard data failed -->
- <string name="reading_vcard_failed_title">Reading of vCard data has failed</string>
+ <!-- The title shown when reading vCard is canceled (probably by a user) -->
+ <string name="reading_vcard_canceled_title">Reading vCard data was canceled</string>
- <!-- Message while reading one vCard file "(current number) of (total number) contacts" The order of "current number" and "total number" cannot be changed (like "total: (total number), current: (current number)")-->
- <string name="reading_vcard_contacts"><xliff:g id="current_number">%s</xliff:g> of <xliff:g id="total_number">%s</xliff:g> contacts</string>
+ <!-- The title shown when reading vCard is canceled (probably by a user) -->
+ <string name="reading_vcard_finished_title">Finished Reading vCard data</string>
- <!-- Message while reading multiple vCard files "(current number) of (total number) files" The order of "current number" and "total number" cannot be changed (like "total: (total number), current: (current number)")-->
- <string name="reading_vcard_files"><xliff:g id="current_number">%s</xliff:g> of <xliff:g id="total_number">%s</xliff:g> files</string>
+ <!-- The message shown when vCard importer started running. -->
+ <string name="vcard_importer_start_message">vCard importer started.</string>
+
+ <!-- The message shown when additional vCard to be imported is given during the import for others -->
+ <string name="vcard_importer_will_start_message">vCard importer will import the vCard after a while.</string>
+
+ <!-- The percentage, used for expressing the progress of vCard import. -->
+ <string name="percentage">%s%%</string>
<!-- Dialog title shown when a user confirms whether he/she export Contact data -->
<string name="confirm_export_title">Confirm export</string>
@@ -1134,4 +1141,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 0d2c7eb..533888f 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,10 +385,15 @@
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;
+ private boolean mRunQueriesSynchronously;
private QueryHandler mQueryHandler;
private boolean mJustCreated;
private boolean mSyncEnabled;
@@ -423,8 +404,6 @@
private Uri mGroupUri;
- private long mQueryAggregateId;
-
private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
private int mWritableSourcesCnt;
private int mReadOnlySourcesCnt;
@@ -459,16 +438,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 +449,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 +577,26 @@
}
};
+ 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);
+ }
+ };
+
+ /**
+ * Visible for testing: makes queries run on the UI thread.
+ */
+ /* package */ void runQueriesSynchronously() {
+ mRunQueriesSynchronously = true;
+ }
+
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
@@ -565,9 +605,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 +625,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 +646,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 +685,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 +721,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 +840,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 +865,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 +879,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 +920,7 @@
getContentResolver().unregisterContentObserver(mProviderStatusObserver);
}
- private void setupListView() {
+ protected void setupListView(ContactItemListAdapter adapter) {
final ListView list = getListView();
final LayoutInflater inflater = getLayoutInflater();
@@ -868,7 +932,7 @@
list.setDividerHeight(0);
list.setOnCreateContextMenuListener(this);
- mAdapter = new ContactItemListAdapter(this);
+ mAdapter = adapter;
setListAdapter(mAdapter);
if (list instanceof PinnedHeaderListView && mAdapter.getDisplaySectionHeadersEnabled()) {
@@ -897,29 +961,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 +983,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);
}
@@ -1081,48 +1072,49 @@
// This query can be performed on the UI thread because
// the API explicitly allows such use.
- Cursor cursor = getContentResolver().query(ProviderStatus.CONTENT_URI, new String[] {
- ProviderStatus.STATUS, ProviderStatus.DATA1
- }, null, null, null);
- try {
- if (cursor.moveToFirst()) {
- int status = cursor.getInt(0);
- if (status != mProviderStatus) {
- mProviderStatus = status;
- switch (status) {
- case ProviderStatus.STATUS_NORMAL:
- mAdapter.notifyDataSetInvalidated();
- if (loadData) {
- startQuery();
- }
- break;
+ Cursor cursor = getContentResolver().query(ProviderStatus.CONTENT_URI,
+ new String[] { ProviderStatus.STATUS, ProviderStatus.DATA1 }, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ int status = cursor.getInt(0);
+ if (status != mProviderStatus) {
+ mProviderStatus = status;
+ switch (status) {
+ case ProviderStatus.STATUS_NORMAL:
+ mAdapter.notifyDataSetInvalidated();
+ if (loadData) {
+ startQuery();
+ }
+ break;
- case ProviderStatus.STATUS_CHANGING_LOCALE:
- messageView.setText(R.string.locale_change_in_progress);
- mAdapter.changeCursor(null);
- mAdapter.notifyDataSetInvalidated();
- break;
+ case ProviderStatus.STATUS_CHANGING_LOCALE:
+ messageView.setText(R.string.locale_change_in_progress);
+ mAdapter.changeCursor(null);
+ mAdapter.notifyDataSetInvalidated();
+ break;
- case ProviderStatus.STATUS_UPGRADING:
- messageView.setText(R.string.upgrade_in_progress);
- mAdapter.changeCursor(null);
- mAdapter.notifyDataSetInvalidated();
- break;
+ case ProviderStatus.STATUS_UPGRADING:
+ messageView.setText(R.string.upgrade_in_progress);
+ mAdapter.changeCursor(null);
+ mAdapter.notifyDataSetInvalidated();
+ break;
- case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY:
- long size = cursor.getLong(1);
- String message = getResources().getString(
- R.string.upgrade_out_of_memory, new Object[] {size});
- messageView.setText(message);
- configureImportFailureView(importFailureView);
- mAdapter.changeCursor(null);
- mAdapter.notifyDataSetInvalidated();
- break;
+ case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY:
+ long size = cursor.getLong(1);
+ String message = getResources().getString(
+ R.string.upgrade_out_of_memory, new Object[] {size});
+ messageView.setText(message);
+ configureImportFailureView(importFailureView);
+ mAdapter.changeCursor(null);
+ mAdapter.notifyDataSetInvalidated();
+ break;
+ }
}
}
+ } finally {
+ cursor.close();
}
- } finally {
- cursor.close();
}
importFailureView.setVisibility(
@@ -1159,7 +1151,7 @@
retryUpgrade.setOnClickListener(listener);
}
- private String getTextFilter() {
+ protected String getTextFilter() {
if (mSearchEditText != null) {
return mSearchEditText.getText().toString();
}
@@ -1191,6 +1183,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 +1194,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 +1216,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 +1235,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 +1289,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 +1327,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 +1349,6 @@
* search text edit.
*/
protected void onSearchTextChanged() {
- // Set the proper empty string
- setEmptyText();
-
Filter filter = mAdapter.getFilter();
filter.filter(getTextFilter());
}
@@ -1306,7 +1356,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 +1599,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 +1784,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 +1870,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 +1896,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 +1933,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 +2139,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;
@@ -2100,6 +2159,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);
}
@@ -2174,7 +2234,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");
}
@@ -2206,7 +2266,6 @@
String[] getProjectionForQuery() {
switch(mMode) {
- case MODE_JOIN_CONTACT:
case MODE_STREQUENT:
case MODE_FREQUENT:
case MODE_STARRED:
@@ -2230,6 +2289,7 @@
return LEGACY_PEOPLE_PROJECTION ;
}
case MODE_QUERY_PICK_PHONE:
+ case MODE_PICK_MULTIPLE_PHONES:
case MODE_PICK_PHONE: {
return PHONES_PROJECTION;
}
@@ -2316,7 +2376,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));
@@ -2344,39 +2404,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();
@@ -2407,6 +2468,10 @@
.build();
}
+ startQuery(uri, projection);
+ }
+
+ protected void startQuery(Uri uri, String[] projection) {
// Kick off the new query
switch (mMode) {
case MODE_GROUP:
@@ -2454,6 +2519,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:
mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
projection, CLAUSE_ONLY_VISIBLE, null, getSortOrder(projection));
@@ -2470,15 +2554,15 @@
ContactMethods.KIND + "=" + android.provider.Contacts.KIND_POSTAL, null,
ContactMethods.DISPLAY_NAME);
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.
*
@@ -2532,6 +2616,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)) {
@@ -2545,30 +2633,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.
@@ -2707,56 +2775,173 @@
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
+ public void startQuery(int token, Object cookie, Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String orderBy) {
+ final ContactsListActivity activity = mActivity.get();
+ if (activity != null && activity.mRunQueriesSynchronously) {
+ Cursor cursor = getContentResolver().query(uri, projection, selection,
+ selectionArgs, orderBy);
+ onQueryComplete(token, cookie, cursor);
+ } else {
+ super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy);
+ }
}
@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();
@@ -2765,12 +2950,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 {
@@ -2779,7 +2977,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;
@@ -2789,8 +2987,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);
@@ -2836,14 +3032,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
@@ -2894,10 +3082,6 @@
return IGNORE_ITEM_VIEW_TYPE;
}
- if (isShowAllContactsItemPosition(position)) {
- return IGNORE_ITEM_VIEW_TYPE;
- }
-
if (isSearchAllContactsItemPosition(position)) {
return IGNORE_ITEM_VIEW_TYPE;
}
@@ -2906,7 +3090,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);
}
@@ -2927,11 +3113,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);
@@ -2946,18 +3127,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);
}
@@ -2965,13 +3141,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;
}
@@ -3000,13 +3176,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) {
@@ -3014,13 +3185,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;
}
@@ -3028,6 +3192,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;
}
@@ -3043,9 +3208,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: {
@@ -3055,6 +3222,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:
@@ -3084,6 +3252,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();
@@ -3103,11 +3280,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 {
@@ -3119,8 +3294,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;
@@ -3253,7 +3428,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) {
@@ -3309,9 +3484,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) {
@@ -3367,8 +3558,7 @@
@Override
public boolean areAllItemsEnabled() {
return mMode != MODE_STARRED
- && !mShowNumberOfContacts
- && mSuggestionsCursorCount == 0;
+ && !mShowNumberOfContacts;
}
@Override
@@ -3379,10 +3569,6 @@
}
position--;
}
-
- if (mSuggestionsCursorCount > 0) {
- return position != 0 && position != mSuggestionsCursorCount + 1;
- }
return position != mFrequentSeparatorPos;
}
@@ -3399,7 +3585,7 @@
superCount++;
}
- if (mSearchMode) {
+ if (mSearchMode && mMode != MODE_PICK_MULTIPLE_PHONES) {
// Last element in the list is the "Find
superCount++;
}
@@ -3410,12 +3596,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 {
@@ -3437,19 +3622,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) {
@@ -3463,10 +3642,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);
@@ -3479,13 +3655,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);
@@ -3582,4 +3752,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..2346467 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -21,32 +21,20 @@
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
-import android.content.ContentResolver;
-import android.content.ContentUris;
+import android.app.Service;
+import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.ServiceConnection;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
+import android.os.IBinder;
import android.os.PowerManager;
-import android.pim.vcard.VCardConfig;
-import android.pim.vcard.VCardEntryCommitter;
-import android.pim.vcard.VCardEntryConstructor;
-import android.pim.vcard.VCardEntryCounter;
-import android.pim.vcard.VCardInterpreter;
-import android.pim.vcard.VCardInterpreterCollection;
-import android.pim.vcard.VCardParser_V21;
-import android.pim.vcard.VCardParser_V30;
-import android.pim.vcard.VCardSourceDetector;
-import android.pim.vcard.exception.VCardException;
-import android.pim.vcard.exception.VCardNestedException;
-import android.pim.vcard.exception.VCardNotSupportedException;
-import android.pim.vcard.exception.VCardVersionException;
-import android.provider.ContactsContract.RawContacts;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
@@ -58,15 +46,12 @@
import java.io.File;
import java.io.IOException;
-import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
import java.util.Set;
import java.util.Vector;
@@ -105,25 +90,23 @@
*/
public class ImportVCardActivity extends Activity {
private static final String LOG_TAG = "ImportVCardActivity";
- private static final boolean DO_PERFORMANCE_PROFILE = false;
+
+ /* package */ static final String VCARD_URI_ARRAY = "vcard_uri_array";
// Run on the UI thread. Must not be null except after onDestroy().
private Handler mHandler = new Handler();
private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
- private Account mAccount;
+ private String mAccountName;
+ private String mAccountType;
private ProgressDialog mProgressDialogForScanVCard;
private List<VCardFile> mAllVCardFileList;
private VCardScanThread mVCardScanThread;
- private VCardReadThread mVCardReadThread;
- private ProgressDialog mProgressDialogForReadVCard;
private String mErrorMessage;
- private boolean mNeedReview = false;
-
// Runs on the UI thread.
private class DialogDisplayer implements Runnable {
private final int mResId;
@@ -152,300 +135,6 @@
private CancelListener mCancelListener = new CancelListener();
- private class VCardReadThread extends Thread
- implements DialogInterface.OnCancelListener {
- private ContentResolver mResolver;
- private VCardParser_V21 mVCardParser;
- private boolean mCanceled;
- private PowerManager.WakeLock mWakeLock;
- private Uri mUri;
- private File mTempFile;
-
- private List<VCardFile> mSelectedVCardFileList;
- private List<String> mErrorFileNameList;
-
- public VCardReadThread(Uri uri) {
- mUri = uri;
- init();
- }
-
- public VCardReadThread(final List<VCardFile> selectedVCardFileList) {
- mSelectedVCardFileList = selectedVCardFileList;
- mErrorFileNameList = new ArrayList<String>();
- init();
- }
-
- private void init() {
- Context context = ImportVCardActivity.this;
- mResolver = context.getContentResolver();
- PowerManager powerManager = (PowerManager)context.getSystemService(
- Context.POWER_SERVICE);
- mWakeLock = powerManager.newWakeLock(
- PowerManager.SCREEN_DIM_WAKE_LOCK |
- PowerManager.ON_AFTER_RELEASE, LOG_TAG);
- }
-
- @Override
- public void finalize() {
- if (mWakeLock != null && mWakeLock.isHeld()) {
- mWakeLock.release();
- }
- }
-
- @Override
- public void run() {
- boolean shouldCallFinish = true;
- mWakeLock.acquire();
- Uri createdUri = null;
- mTempFile = null;
- // Some malicious vCard data may make this thread broken
- // (e.g. OutOfMemoryError).
- // Even in such cases, some should be done.
- try {
- if (mUri != null) { // Read one vCard expressed by mUri
- final Uri targetUri = mUri;
- mProgressDialogForReadVCard.setProgressNumberFormat("");
- mProgressDialogForReadVCard.setProgress(0);
-
- // Count the number of VCard entries
- mProgressDialogForReadVCard.setIndeterminate(true);
- long start;
- if (DO_PERFORMANCE_PROFILE) {
- start = System.currentTimeMillis();
- }
- VCardEntryCounter counter = new VCardEntryCounter();
- VCardSourceDetector detector = new VCardSourceDetector();
- VCardInterpreterCollection builderCollection = new VCardInterpreterCollection(
- Arrays.asList(counter, detector));
- boolean result;
- try {
- result = readOneVCardFile(targetUri,
- VCardConfig.DEFAULT_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);
- } catch (VCardNestedException e2) {
- result = false;
- Log.e(LOG_TAG, "Must not reach here. " + e2);
- }
- }
- if (DO_PERFORMANCE_PROFILE) {
- long time = System.currentTimeMillis() - start;
- Log.d(LOG_TAG, "time for counting the number of vCard entries: " +
- time + " ms");
- }
- if (!result) {
- shouldCallFinish = false;
- return;
- }
-
- mProgressDialogForReadVCard.setProgressNumberFormat(
- getString(R.string.reading_vcard_contacts));
- mProgressDialogForReadVCard.setIndeterminate(false);
- mProgressDialogForReadVCard.setMax(counter.getCount());
- String charset = detector.getEstimatedCharset();
- createdUri = doActuallyReadOneVCard(targetUri, null, charset, true, detector,
- mErrorFileNameList);
- } else { // Read multiple files.
- mProgressDialogForReadVCard.setProgressNumberFormat(
- getString(R.string.reading_vcard_files));
- mProgressDialogForReadVCard.setMax(mSelectedVCardFileList.size());
- mProgressDialogForReadVCard.setProgress(0);
-
- for (VCardFile vcardFile : mSelectedVCardFileList) {
- if (mCanceled) {
- return;
- }
- // TODO: detect scheme!
- final Uri targetUri =
- Uri.parse("file://" + vcardFile.getCanonicalPath());
-
- VCardSourceDetector detector = new VCardSourceDetector();
- try {
- if (!readOneVCardFile(targetUri, VCardConfig.DEFAULT_CHARSET,
- detector, null, true, mErrorFileNameList)) {
- continue;
- }
- } catch (VCardNestedException e) {
- // Assume that VCardSourceDetector was able to detect the source.
- }
- String charset = detector.getEstimatedCharset();
- doActuallyReadOneVCard(targetUri, mAccount,
- charset, false, detector, mErrorFileNameList);
- mProgressDialogForReadVCard.incrementProgressBy(1);
- }
- }
- } finally {
- mWakeLock.release();
- mProgressDialogForReadVCard.dismiss();
- if (mTempFile != null) {
- if (!mTempFile.delete()) {
- Log.w(LOG_TAG, "Failed to delete a cache file.");
- }
- mTempFile = null;
- }
- // finish() is called via mCancelListener, which is used in DialogDisplayer.
- if (shouldCallFinish && !isFinishing()) {
- if (mErrorFileNameList == null || mErrorFileNameList.isEmpty()) {
- finish();
- if (mNeedReview) {
- mNeedReview = false;
- Log.v("importVCardActivity", "Prepare to review the imported contact");
-
- if (createdUri != null) {
- // get contact_id of this raw_contact
- final long rawContactId = ContentUris.parseId(createdUri);
- Uri contactUri = RawContacts.getContactLookupUri(
- getContentResolver(), ContentUris.withAppendedId(
- RawContacts.CONTENT_URI, rawContactId));
-
- Intent viewIntent = new Intent(Intent.ACTION_VIEW, contactUri);
- startActivity(viewIntent);
- }
- }
- } else {
- StringBuilder builder = new StringBuilder();
- boolean first = true;
- for (String fileName : mErrorFileNameList) {
- if (first) {
- first = false;
- } else {
- builder.append(", ");
- }
- builder.append(fileName);
- }
-
- runOnUIThread(new DialogDisplayer(
- getString(R.string.fail_reason_failed_to_read_files,
- builder.toString())));
- }
- }
- }
- }
-
- private Uri doActuallyReadOneVCard(Uri uri, Account account,
- String charset, boolean showEntryParseProgress,
- VCardSourceDetector detector, List<String> errorFileNameList) {
- final Context context = ImportVCardActivity.this;
- VCardEntryConstructor builder;
- final String currentLanguage = Locale.getDefault().getLanguage();
- int vcardType = VCardConfig.getVCardTypeFromString(
- context.getString(R.string.config_import_vcard_type));
- if (charset != null) {
- builder = new VCardEntryConstructor(charset, charset, false, vcardType, mAccount);
- } else {
- charset = VCardConfig.DEFAULT_CHARSET;
- builder = new VCardEntryConstructor(null, null, false, vcardType, mAccount);
- }
- VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
- builder.addEntryHandler(committer);
- if (showEntryParseProgress) {
- builder.addEntryHandler(new ProgressShower(mProgressDialogForReadVCard,
- context.getString(R.string.reading_vcard_message),
- ImportVCardActivity.this,
- mHandler));
- }
-
- try {
- if (!readOneVCardFile(uri, charset, builder, detector, false, null)) {
- return null;
- }
- } catch (VCardNestedException e) {
- Log.e(LOG_TAG, "Never reach here.");
- }
- final ArrayList<Uri> createdUris = committer.getCreatedUris();
- return (createdUris == null || createdUris.size() != 1) ? null : createdUris.get(0);
- }
-
- private boolean readOneVCardFile(Uri uri, String charset,
- VCardInterpreter builder, VCardSourceDetector detector,
- boolean throwNestedException, List<String> errorFileNameList)
- throws VCardNestedException {
- InputStream is;
- try {
- is = mResolver.openInputStream(uri);
- mVCardParser = new VCardParser_V21(detector);
-
- try {
- mVCardParser.parse(is, charset, builder, mCanceled);
- } catch (VCardVersionException e1) {
- try {
- is.close();
- } catch (IOException e) {
- }
- if (builder instanceof VCardEntryConstructor) {
- // Let the object clean up internal temporal objects,
- ((VCardEntryConstructor)builder).clear();
- }
- is = mResolver.openInputStream(uri);
-
- try {
- mVCardParser = new VCardParser_V30();
- mVCardParser.parse(is, charset, builder, mCanceled);
- } catch (VCardVersionException e2) {
- throw new VCardException("vCard with unspported version.");
- }
- } finally {
- if (is != null) {
- try {
- is.close();
- } catch (IOException e) {
- }
- }
- }
- } catch (IOException e) {
- Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
-
- mProgressDialogForReadVCard.dismiss();
-
- if (errorFileNameList != null) {
- errorFileNameList.add(uri.toString());
- } else {
- runOnUIThread(new DialogDisplayer(
- getString(R.string.fail_reason_io_error) +
- ": " + e.getLocalizedMessage()));
- }
- return false;
- } catch (VCardNotSupportedException e) {
- if ((e instanceof VCardNestedException) && throwNestedException) {
- throw (VCardNestedException)e;
- }
- if (errorFileNameList != null) {
- errorFileNameList.add(uri.toString());
- } else {
- runOnUIThread(new DialogDisplayer(
- getString(R.string.fail_reason_vcard_not_supported_error) +
- " (" + e.getMessage() + ")"));
- }
- return false;
- } catch (VCardException e) {
- if (errorFileNameList != null) {
- errorFileNameList.add(uri.toString());
- } else {
- runOnUIThread(new DialogDisplayer(
- getString(R.string.fail_reason_vcard_parse_error) +
- " (" + e.getMessage() + ")"));
- }
- return false;
- }
- return true;
- }
-
- public void cancel() {
- mCanceled = true;
- if (mVCardParser != null) {
- mVCardParser.cancel();
- }
- }
-
- public void onCancel(DialogInterface dialog) {
- cancel();
- }
- }
-
private class ImportTypeSelectedListener implements
DialogInterface.OnClickListener {
public static final int IMPORT_ONE = 0;
@@ -459,7 +148,7 @@
if (which == DialogInterface.BUTTON_POSITIVE) {
switch (mCurrentIndex) {
case IMPORT_ALL:
- importMultipleVCardFromSDCard(mAllVCardFileList);
+ importVCardFromSDCard(mAllVCardFileList);
break;
case IMPORT_MULTIPLE:
showDialog(R.id.dialog_select_multiple_vcard);
@@ -492,18 +181,16 @@
if (which == DialogInterface.BUTTON_POSITIVE) {
if (mSelectedIndexSet != null) {
List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>();
- int size = mAllVCardFileList.size();
+ final int size = mAllVCardFileList.size();
// We'd like to sort the files by its index, so we do not use Set iterator.
for (int i = 0; i < size; i++) {
if (mSelectedIndexSet.contains(i)) {
selectedVCardFileList.add(mAllVCardFileList.get(i));
}
}
- importMultipleVCardFromSDCard(selectedVCardFileList);
+ importVCardFromSDCard(selectedVCardFileList);
} else {
- String canonicalPath = mAllVCardFileList.get(mCurrentIndex).getCanonicalPath();
- final Uri uri = Uri.parse("file://" + canonicalPath);
- importOneVCardFromSDCard(uri);
+ importVCardFromSDCard(mAllVCardFileList.get(mCurrentIndex));
}
} else if (which == DialogInterface.BUTTON_NEGATIVE) {
finish();
@@ -642,12 +329,9 @@
private void startVCardSelectAndImport() {
int size = mAllVCardFileList.size();
- if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically)) {
- importMultipleVCardFromSDCard(mAllVCardFileList);
- } else if (size == 1) {
- String canonicalPath = mAllVCardFileList.get(0).getCanonicalPath();
- Uri uri = Uri.parse("file://" + canonicalPath);
- importOneVCardFromSDCard(uri);
+ if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically) ||
+ size == 1) {
+ importVCardFromSDCard(mAllVCardFileList);
} else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) {
runOnUIThread(new DialogDisplayer(R.id.dialog_select_import_type));
} else {
@@ -655,53 +339,70 @@
}
}
- private void importMultipleVCardFromSDCard(final List<VCardFile> selectedVCardFileList) {
- runOnUIThread(new Runnable() {
- public void run() {
- mVCardReadThread = new VCardReadThread(selectedVCardFileList);
- showDialog(R.id.dialog_reading_vcard);
- }
- });
+ private void importVCardFromSDCard(final List<VCardFile> selectedVCardFileList) {
+ final int size = selectedVCardFileList.size();
+ String[] uriStrings = new String[size];
+ int i = 0;
+ for (VCardFile vcardFile : selectedVCardFileList) {
+ uriStrings[i] = "file://" + vcardFile.getCanonicalPath();
+ i++;
+ }
+ importVCard(uriStrings);
+ }
+
+ private void importVCardFromSDCard(final VCardFile vcardFile) {
+ String[] uriStrings = new String[1];
+ uriStrings[0] = "file://" + vcardFile.getCanonicalPath();
+ importVCard(uriStrings);
}
- private void importOneVCardFromSDCard(final Uri uri) {
- runOnUIThread(new Runnable() {
- public void run() {
- mVCardReadThread = new VCardReadThread(uri);
- showDialog(R.id.dialog_reading_vcard);
- }
- });
+ private void importVCard(final String uriString) {
+ String[] uriStrings = new String[1];
+ uriStrings[0] = uriString;
+ importVCard(uriStrings);
+ }
+
+ private void importVCard(final String[] uriStrings) {
+ final Intent intent = new Intent(this, ImportVCardService.class);
+ intent.putExtra(VCARD_URI_ARRAY, uriStrings);
+ intent.putExtra("account_name", mAccountName);
+ intent.putExtra("account_type", mAccountType);
+
+ // TODO: permission is not migrated to ImportVCardService, so some exception is
+ // thrown when reading some Uri, permission of which is temporarily guaranteed
+ // to ImportVCardActivity, not ImportVCardService.
+ startService(intent);
+ finish();
}
private Dialog getSelectImportTypeDialog() {
- DialogInterface.OnClickListener listener =
- new ImportTypeSelectedListener();
- AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(R.string.select_vcard_title)
- .setPositiveButton(android.R.string.ok, listener)
- .setOnCancelListener(mCancelListener)
- .setNegativeButton(android.R.string.cancel, mCancelListener);
+ final DialogInterface.OnClickListener listener = new ImportTypeSelectedListener();
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.select_vcard_title)
+ .setPositiveButton(android.R.string.ok, listener)
+ .setOnCancelListener(mCancelListener)
+ .setNegativeButton(android.R.string.cancel, mCancelListener);
- String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE];
+ final String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE];
items[ImportTypeSelectedListener.IMPORT_ONE] =
- getString(R.string.import_one_vcard_string);
+ getString(R.string.import_one_vcard_string);
items[ImportTypeSelectedListener.IMPORT_MULTIPLE] =
- getString(R.string.import_multiple_vcard_string);
+ getString(R.string.import_multiple_vcard_string);
items[ImportTypeSelectedListener.IMPORT_ALL] =
- getString(R.string.import_all_vcard_string);
+ getString(R.string.import_all_vcard_string);
builder.setSingleChoiceItems(items, ImportTypeSelectedListener.IMPORT_ONE, listener);
return builder.create();
}
private Dialog getVCardFileSelectDialog(boolean multipleSelect) {
- int size = mAllVCardFileList.size();
- VCardSelectedListener listener = new VCardSelectedListener(multipleSelect);
- AlertDialog.Builder builder =
- new AlertDialog.Builder(this)
- .setTitle(R.string.select_vcard_title)
- .setPositiveButton(android.R.string.ok, listener)
- .setOnCancelListener(mCancelListener)
- .setNegativeButton(android.R.string.cancel, mCancelListener);
+ final int size = mAllVCardFileList.size();
+ final VCardSelectedListener listener = new VCardSelectedListener(multipleSelect);
+ final AlertDialog.Builder builder =
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.select_vcard_title)
+ .setPositiveButton(android.R.string.ok, listener)
+ .setOnCancelListener(mCancelListener)
+ .setNegativeButton(android.R.string.cancel, mCancelListener);
CharSequence[] items = new CharSequence[size];
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@@ -735,17 +436,14 @@
final Intent intent = getIntent();
if (intent != null) {
- final String accountName = intent.getStringExtra("account_name");
- final String accountType = intent.getStringExtra("account_type");
- if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
- mAccount = new Account(accountName, accountType);
- }
+ mAccountName = intent.getStringExtra("account_name");
+ mAccountType = intent.getStringExtra("account_type");
} else {
Log.e(LOG_TAG, "intent does not exist");
}
// The caller often does not know account information at all, so we show the UI instead.
- if (mAccount == null) {
+ if (TextUtils.isEmpty(mAccountName) || TextUtils.isEmpty(mAccountType)) {
// There's three possibilities:
// - more than one accounts -> ask the user
// - just one account -> use the account without asking the user
@@ -761,7 +459,9 @@
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
- mAccount = mAccountList.get(which);
+ final Account account = mAccountList.get(which);
+ mAccountName = account.name;
+ mAccountType = account.type;
// Instead of using Intent mechanism, call the relevant private method,
// to avoid throwing an Intent to itself again.
startImport();
@@ -770,7 +470,11 @@
showDialog(resId);
return;
} else {
- mAccount = size > 0 ? accountList.get(0) : null;
+ final Account account = ((size > 0) ? accountList.get(0) : null);
+ if (account != null) {
+ mAccountName = account.name;
+ mAccountType = account.type;
+ }
}
}
@@ -782,13 +486,9 @@
final String action = intent.getAction();
final Uri uri = intent.getData();
Log.v(LOG_TAG, "action = " + action + " ; path = " + uri);
- if (Intent.ACTION_VIEW.equals(action)) {
- // Import the file directly and then go to EDIT screen
- mNeedReview = true;
- }
if (uri != null) {
- importOneVCardFromSDCard(uri);
+ importVCard(uri.toString());
} else {
doScanExternalStorageAndImportVCard();
}
@@ -845,19 +545,6 @@
case R.id.dialog_select_one_vcard: {
return getVCardFileSelectDialog(false);
}
- case R.id.dialog_reading_vcard: {
- if (mProgressDialogForReadVCard == null) {
- String title = getString(R.string.reading_vcard_title);
- String message = getString(R.string.reading_vcard_message);
- mProgressDialogForReadVCard = new ProgressDialog(this);
- mProgressDialogForReadVCard.setTitle(title);
- mProgressDialogForReadVCard.setMessage(message);
- mProgressDialogForReadVCard.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
- mProgressDialogForReadVCard.setOnCancelListener(mVCardReadThread);
- mVCardReadThread.start();
- }
- return mProgressDialogForReadVCard;
- }
case R.id.dialog_io_exception: {
String message = (getString(R.string.scanning_sdcard_failed_message,
getString(R.string.fail_reason_io_error)));
@@ -891,11 +578,6 @@
@Override
protected void onPause() {
super.onPause();
- if (mVCardReadThread != null) {
- // The Activity is no longer visible. Stop the thread.
- mVCardReadThread.cancel();
- mVCardReadThread = null;
- }
// ImportVCardActivity should not be persistent. In other words, if there's some
// event calling onPause(), this Activity should finish its work and give the main
@@ -912,29 +594,6 @@
// make sure that the handler does not run any callback when
// this activity isFinishing().
- // Need to make sure any worker thread is done before we flush and
- // nullify the message handler.
- if (mVCardReadThread != null) {
- Log.w(LOG_TAG, "VCardReadThread exists while this Activity is now being killed!");
- mVCardReadThread.cancel();
- int attempts = 0;
- while (mVCardReadThread.isAlive() && attempts < 10) {
- try {
- Thread.currentThread().sleep(20);
- } catch (InterruptedException ie) {
- // Keep on going until max attempts is reached.
- }
- attempts++;
- }
- if (mVCardReadThread.isAlive()) {
- // Find out why the thread did not exit in a timely
- // fashion. Last resort: increase the sleep duration
- // and/or the number of attempts.
- Log.e(LOG_TAG, "VCardReadThread is still alive after max attempts.");
- }
- mVCardReadThread = null;
- }
-
// Callbacks messages have what == 0.
if (mHandler.hasMessages(0)) {
mHandler.removeMessages(0);
@@ -955,17 +614,6 @@
}
}
- @Override
- public void finalize() {
- // TODO: This should not be needed. Throw exception instead.
- if (mVCardReadThread != null) {
- // Not sure this procedure is really needed, but just in case...
- Log.e(LOG_TAG, "VCardReadThread exists while this Activity is now being killed!");
- mVCardReadThread.cancel();
- mVCardReadThread = null;
- }
- }
-
/**
* Scans vCard in external storage (typically SDCard) and tries to import it.
* - When there's no SDCard available, an error dialog is shown.
diff --git a/src/com/android/contacts/ImportVCardService.java b/src/com/android/contacts/ImportVCardService.java
new file mode 100644
index 0000000..aea47cb
--- /dev/null
+++ b/src/com/android/contacts/ImportVCardService.java
@@ -0,0 +1,462 @@
+/*
+ * 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.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.IBinder;
+import android.pim.vcard.VCardConfig;
+import android.pim.vcard.VCardEntry;
+import android.pim.vcard.VCardEntryCommitter;
+import android.pim.vcard.VCardEntryConstructor;
+import android.pim.vcard.VCardEntryCounter;
+import android.pim.vcard.VCardEntryHandler;
+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;
+import android.pim.vcard.exception.VCardException;
+import android.pim.vcard.exception.VCardNestedException;
+import android.pim.vcard.exception.VCardNotSupportedException;
+import android.pim.vcard.exception.VCardVersionException;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.RemoteViews;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+/**
+ * The class responsible for importing vCard from one ore multiple Uris.
+ */
+public class ImportVCardService extends Service {
+ private final static String LOG_TAG = "ImportVCardService";
+
+ private class ProgressNotifier implements VCardEntryHandler {
+ private final int mId;
+
+ public ProgressNotifier(int id) {
+ mId = id;
+ }
+
+ public void onStart() {
+ }
+
+ public void onEntryCreated(VCardEntry contactStruct) {
+ mCurrentCount++; // 1 origin.
+ if (contactStruct.isIgnorable()) {
+ return;
+ }
+
+ final Context context = ImportVCardService.this;
+ // We don't use startEntry() since:
+ // - We cannot know name there but here.
+ // - There's high probability where name comes soon after the beginning of entry, so
+ // we don't need to hurry to show something.
+ final String packageName = "com.android.contacts";
+ final RemoteViews remoteViews = new RemoteViews(packageName,
+ R.layout.status_bar_ongoing_event_progress_bar);
+ final String title = getString(R.string.reading_vcard_title);
+ final String text = getString(R.string.progress_notifier_message,
+ String.valueOf(mCurrentCount),
+ String.valueOf(mTotalCount),
+ contactStruct.getDisplayName());
+
+ // TODO: uploading image does not work correctly. (looks like a static image).
+ remoteViews.setTextViewText(R.id.description, text);
+ remoteViews.setProgressBar(R.id.progress_bar, mTotalCount, mCurrentCount,
+ mTotalCount == -1);
+ final String percentage =
+ getString(R.string.percentage,
+ String.valueOf(mCurrentCount * 100/mTotalCount));
+ remoteViews.setTextViewText(R.id.progress_text, percentage);
+ remoteViews.setImageViewResource(R.id.appIcon, android.R.drawable.stat_sys_download);
+
+ final Notification notification = new Notification();
+ notification.icon = android.R.drawable.stat_sys_download;
+ notification.flags |= Notification.FLAG_ONGOING_EVENT;
+ notification.contentView = remoteViews;
+
+ notification.contentIntent =
+ PendingIntent.getActivity(context, 0,
+ new Intent(context, ContactsListActivity.class), 0);
+ mNotificationManager.notify(mId, notification);
+ }
+
+ public void onEnd() {
+ }
+ }
+
+ private class VCardReadThread extends Thread {
+ private final Context mContext;
+ private final ContentResolver mResolver;
+ private final int mPreferedVCardType;
+ private VCardParser mVCardParser;
+ private boolean mCanceled;
+ private final List<Uri> mErrorUris;
+ private final List<Uri> mCreatedUris;
+
+ public VCardReadThread() {
+ mContext = ImportVCardService.this;
+ mResolver = mContext.getContentResolver();
+ mPreferedVCardType = VCardConfig.getVCardTypeFromString(
+ mContext.getString(R.string.config_import_vcard_type));
+ mErrorUris = new ArrayList<Uri>();
+ mCreatedUris = new ArrayList<Uri>();
+ }
+
+ @Override
+ public void run() {
+ while (!mCanceled) {
+ final Account account;
+ final Uri[] uris;
+ final int id;
+ final boolean needReview;
+ synchronized (mContext) {
+ if (mPendingInputs.size() == 0) {
+ mNowRunning = false;
+ break;
+ } else {
+ final PendingInput pendingInput = mPendingInputs.poll();
+ account = pendingInput.account;
+ uris = pendingInput.uris;
+ id = pendingInput.id;
+ }
+ }
+ runInternal(account, uris, id);
+ doFinishNotification(id, uris);
+ mErrorUris.clear();
+ mCreatedUris.clear();
+ }
+ Log.i(LOG_TAG, "Successfully imported. Total: " + mTotalCount);
+ stopSelf();
+ }
+
+ private void runInternal(Account account, Uri[] uris, int id) {
+ int totalCount = 0;
+ final ArrayList<VCardSourceDetector> detectorList =
+ new ArrayList<VCardSourceDetector>();
+ final String defaultCharset = VCardConfig.DEFAULT_IMPORT_CHARSET;
+ // First scan all Uris with a default charset and try to understand an exact
+ // charset to be used to each Uri. Note that detector would return null when
+ // it does not know an appropriate charset, so stick to use the default
+ // at that time.
+ // TODO: notification for first scanning?
+ for (Uri uri : uris) {
+ if (mCanceled) {
+ return;
+ }
+ final VCardEntryCounter counter = new VCardEntryCounter();
+ final VCardSourceDetector detector = new VCardSourceDetector();
+ final VCardInterpreterCollection interpreterCollection =
+ new VCardInterpreterCollection(Arrays.asList(counter, detector));
+ try {
+ if (!readOneVCard(uri, defaultCharset,
+ interpreterCollection, null, true)) {
+ mErrorUris.add(uri);
+ }
+ } catch (VCardNestedException e) {
+ // Assume that VCardSourceDetector was able to detect the source.
+ }
+
+ totalCount += counter.getCount();
+ detectorList.add(detector);
+ }
+
+ if (mErrorUris.size() > 0) {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("Error happened on ");
+ for (Uri errorUri : mErrorUris) {
+ builder.append("\"");
+ builder.append(errorUri.toString());
+ builder.append("\"");
+ }
+ Log.e(LOG_TAG, builder.toString());
+ doErrorNotification(id);
+ return;
+ }
+
+ if (uris.length != detectorList.size()) {
+ Log.e(LOG_TAG,
+ "The number of Uris to be imported is different from that of " +
+ "charset to be used.");
+ doErrorNotification(id);
+ return;
+ }
+
+ // First scanning is over. Try to import each vCard, which causes side effects.
+ mTotalCount = totalCount;
+ mCurrentCount = 0;
+
+ for (int i = 0; i < uris.length; i++) {
+ if (mCanceled) {
+ Log.w(LOG_TAG, "Canceled during importing (with storing data in database)");
+ // TODO: implement cancel correctly.
+ return;
+ }
+ final Uri uri = uris[i];
+ final VCardSourceDetector detector = detectorList.get(i);
+ final int vcardType = mPreferedVCardType;
+ String charset = detector.getEstimatedCharset();
+ if (charset == null) {
+ charset = defaultCharset;
+ }
+ final VCardEntryConstructor constructor =
+ new VCardEntryConstructor(charset, charset, false, vcardType, account);
+ final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
+ final ProgressNotifier notifier = new ProgressNotifier(id);
+ constructor.addEntryHandler(committer);
+ constructor.addEntryHandler(notifier);
+
+ try {
+ if (!readOneVCard(uri, charset, constructor, detector, false)) {
+ Log.e(LOG_TAG, "Failed to read \"" + uri.toString() + "\" " +
+ "while first scan was successful.");
+ }
+ } catch (VCardNestedException e) {
+ // We should already know the number of nests in the first scan and
+ // treat them at this time.
+ Log.e(LOG_TAG, "Must not reach here.");
+ }
+ final List<Uri> createdUris = committer.getCreatedUris();
+ if (createdUris != null && createdUris.size() > 0) {
+ mCreatedUris.addAll(createdUris);
+ } else {
+ Log.w(LOG_TAG, "Created Uris is null (src = " + uri.toString() + "\"");
+ }
+ }
+ }
+
+ private boolean readOneVCard(Uri uri, String charset, VCardInterpreter interpreter,
+ VCardSourceDetector detector, boolean throwNestedException)
+ throws VCardNestedException {
+ InputStream is;
+ try {
+ is = mResolver.openInputStream(uri);
+
+ // We need synchronized since we need to handle mCanceled and mVCardParser
+ // at once. In the worst case, a user may call cancel() just before recreating
+ // mVCardParser.
+ synchronized (this) {
+ mVCardParser = new VCardParser_V21(detector);
+ if (mCanceled) {
+ mVCardParser.cancel();
+ }
+ }
+
+ try {
+ mVCardParser.parse(is, charset, interpreter);
+ } catch (VCardVersionException e1) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ if (interpreter instanceof VCardEntryConstructor) {
+ // Let the object clean up internal temporal objects,
+ ((VCardEntryConstructor) interpreter).clear();
+ }
+ is = mResolver.openInputStream(uri);
+
+ synchronized (this) {
+ mVCardParser = new VCardParser_V30();
+ if (mCanceled) {
+ mVCardParser.cancel();
+ }
+ }
+
+ try {
+ mVCardParser.parse(is, charset, interpreter);
+ } catch (VCardVersionException e2) {
+ throw new VCardException("vCard with unspported version.");
+ }
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
+ return false;
+ } catch (VCardNotSupportedException e) {
+ if ((e instanceof VCardNestedException) && throwNestedException) {
+ throw (VCardNestedException) e;
+ }
+ return false;
+ } catch (VCardException e) {
+ return false;
+ }
+ return true;
+ }
+
+ private void doErrorNotification(int id) {
+ final Notification notification = new Notification();
+ notification.icon = android.R.drawable.stat_sys_download_done;
+ final String title = mContext.getString(R.string.reading_vcard_failed_title);
+ final PendingIntent intent =
+ PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+ notification.setLatestEventInfo(mContext, title, "", intent);
+ mNotificationManager.notify(id, notification);
+ }
+
+ private void doFinishNotification(int id, Uri[] uris) {
+ final Notification notification = new Notification();
+ notification.icon = android.R.drawable.stat_sys_download_done;
+ final String title = mContext.getString(R.string.reading_vcard_finished_title);
+
+ final Intent intent;
+ final long rawContactId = ContentUris.parseId(mCreatedUris.get(0));
+ final Uri contactUri = RawContacts.getContactLookupUri(
+ getContentResolver(), ContentUris.withAppendedId(
+ RawContacts.CONTENT_URI, rawContactId));
+ intent = new Intent(Intent.ACTION_VIEW, contactUri);
+
+ final String text = ((uris.length == 1) ? uris[0].getPath() : "");
+ final PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, intent, 0);
+ notification.setLatestEventInfo(mContext, title, text, pendingIntent);
+ mNotificationManager.notify(id, notification);
+ }
+
+ // We need synchronized since we need to handle mCanceled and mVCardParser at once.
+ public synchronized void cancel() {
+ mCanceled = true;
+ if (mVCardParser != null) {
+ mVCardParser.cancel();
+ }
+ }
+
+ public void onCancel(DialogInterface dialog) {
+ cancel();
+ }
+ }
+
+ private static class PendingInput {
+ public final Account account;
+ public final Uri[] uris;
+ public final int id;
+
+ public PendingInput(Account account, Uri[] uris, int id) {
+ this.account = account;
+ this.uris = uris;
+ this.id = id;
+ }
+ }
+
+ // The two classes bellow must be called inside the synchronized block, using this context.
+ private boolean mNowRunning;
+ private final Queue<PendingInput> mPendingInputs = new LinkedList<PendingInput>();
+
+ private NotificationManager mNotificationManager;
+ private Thread mThread;
+ private int mTotalCount;
+ private int mCurrentCount;
+
+ private Uri[] tryGetUris(Intent intent) {
+ final String[] uriStrings =
+ intent.getStringArrayExtra(ImportVCardActivity.VCARD_URI_ARRAY);
+ if (uriStrings == null || uriStrings.length == 0) {
+ Log.e(LOG_TAG, "Given uri array is empty");
+ return null;
+ }
+
+ final int length = uriStrings.length;
+ final Uri[] uris = new Uri[length];
+ for (int i = 0; i < length; i++) {
+ uris[i] = Uri.parse(uriStrings[i]);
+ }
+
+ return uris;
+ }
+
+ private Account tryGetAccount(Intent intent) {
+ if (intent == null) {
+ Log.w(LOG_TAG, "Intent is null");
+ return null;
+ }
+
+ final String accountName = intent.getStringExtra("account_name");
+ final String accountType = intent.getStringExtra("account_type");
+ if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+ return new Account(accountName, accountType);
+ } else {
+ Log.w(LOG_TAG, "Account is not set.");
+ return null;
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (mNotificationManager == null) {
+ mNotificationManager =
+ (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ final Account account = tryGetAccount(intent);
+ final Uri[] uris = tryGetUris(intent);
+ if (uris == null) {
+ Log.e(LOG_TAG, "Uris are null.");
+ Toast.makeText(this, getString(R.string.reading_vcard_failed_title),
+ Toast.LENGTH_LONG).show();
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+
+ synchronized (this) {
+ mPendingInputs.add(new PendingInput(account, uris, startId));
+ if (!mNowRunning) {
+ Toast.makeText(this, getString(R.string.vcard_importer_start_message),
+ Toast.LENGTH_LONG).show();
+ // Assume thread is alredy broken.
+ // Even when it still exists, it never scan the PendingInput newly added above.
+ mNowRunning = true;
+ mThread = new VCardReadThread();
+ mThread.start();
+ } else {
+ Toast.makeText(this, getString(R.string.vcard_importer_will_start_message),
+ Toast.LENGTH_LONG).show();
+ }
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+}
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/ProgressShower.java b/src/com/android/contacts/ProgressShower.java
deleted file mode 100644
index a5ad2a2..0000000
--- a/src/com/android/contacts/ProgressShower.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.contacts;
-
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.os.Handler;
-import android.pim.vcard.VCardEntry;
-import android.pim.vcard.VCardEntryHandler;
-import android.pim.vcard.VCardConfig;
-import android.util.Log;
-
-public class ProgressShower implements VCardEntryHandler {
- public static final String LOG_TAG = "vcard.ProgressShower";
-
- private final Context mContext;
- private final Handler mHandler;
- private final ProgressDialog mProgressDialog;
- private final String mProgressMessage;
-
- private long mTime;
-
- private class ShowProgressRunnable implements Runnable {
- private VCardEntry mContact;
-
- public ShowProgressRunnable(VCardEntry contact) {
- mContact = contact;
- }
-
- public void run() {
- mProgressDialog.setMessage( mProgressMessage + "\n" +
- mContact.getDisplayName());
- mProgressDialog.incrementProgressBy(1);
- }
- }
-
- public ProgressShower(ProgressDialog progressDialog,
- String progressMessage,
- Context context,
- Handler handler) {
- mContext = context;
- mHandler = handler;
- mProgressDialog = progressDialog;
- mProgressMessage = progressMessage;
- }
-
- public void onStart() {
- }
-
- public void onEntryCreated(VCardEntry contactStruct) {
- long start = System.currentTimeMillis();
-
- if (!contactStruct.isIgnorable()) {
- if (mProgressDialog != null && mProgressMessage != null) {
- if (mHandler != null) {
- mHandler.post(new ShowProgressRunnable(contactStruct));
- } else {
- mProgressDialog.setMessage(mContext.getString(R.string.progress_shower_message,
- mProgressMessage,
- contactStruct.getDisplayName()));
- }
- }
- }
-
- mTime += System.currentTimeMillis() - start;
- }
-
- public void onEnd() {
- if (VCardConfig.showPerformanceLog()) {
- Log.d(LOG_TAG,
- String.format("Time to progress a dialog: %d ms", mTime));
- }
- }
-}
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..dc14770
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -0,0 +1,145 @@
+/*
+ * 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.mvcframework.LoaderActivity;
+import com.android.contacts.views.detail.ContactDetailView;
+import com.android.contacts.views.detail.ContactLoader;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class ContactDetailActivity extends LoaderActivity<ContactLoader.Result> implements
+ DialogManager.DialogShowingViewActivity {
+ private static final int LOADER_DETAILS = 1;
+ private ContactDetailView mDetails;
+ 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));
+ }
+
+ @Override
+ public void onInitializeLoaders() {
+ startLoading(LOADER_DETAILS, null);
+ }
+
+ @Override
+ protected ContactLoader onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_DETAILS: {
+ return new ContactLoader(this, getIntent().getData());
+ }
+ }
+ return null;
+ }
+
+
+ @Override
+ public void onLoadComplete(int id, ContactLoader.Result data) {
+ switch (id) {
+ case LOADER_DETAILS:
+ if (data == ContactLoader.Result.NOT_FOUND) {
+ // Item has been deleted
+ Log.i(TAG, "No contact found. Closing activity");
+ finish();
+ return;
+ }
+ mDetails.setData(data);
+ break;
+ }
+ }
+
+ @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..25585be
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/CursorLoader.java
@@ -0,0 +1,105 @@
+/*
+ * 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<Cursor> {
+ Cursor mCursor;
+ ForceLoadContentObserver mObserver;
+ boolean mClosed;
+
+ 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;
+ deliverResult(cursor);
+ }
+ }
+
+ public CursorLoader(Context context) {
+ super(context);
+ 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) {
+ deliverResult(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..fab2f46
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/Loader.java
@@ -0,0 +1,119 @@
+/*
+ * 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.ContentObserver;
+import android.os.Handler;
+
+public abstract class Loader<D> {
+ private int mId;
+ private OnLoadCompleteListener<D> mListener;
+ private Context mContext;
+
+ protected final class ForceLoadContentObserver extends ContentObserver {
+ public ForceLoadContentObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ forceLoad();
+ }
+ }
+
+ public interface OnLoadCompleteListener<D> {
+ public void onLoadComplete(int id, D data);
+ }
+
+ protected void deliverResult(D data) {
+ if (mListener != null) {
+ mListener.onLoadComplete(mId, data);
+
+ }
+ }
+
+ public Loader(Context context) {
+ mContext = context.getApplicationContext();
+ }
+
+ /**
+ * @return an application context
+ */
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * 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 registerListener(int id, OnLoadCompleteListener<D> listener) {
+// if (mListener != null) {
+ // throw new IllegalStateException("There is already a listener registered");
+ // }
+ mListener = listener;
+ mId = id;
+ }
+
+ /**
+ * Must be called from the UI thread
+ */
+ public void unregisterListener(OnLoadCompleteListener<D> listener) {
+ if (mListener == null) {
+ throw new IllegalStateException("No listener register");
+ }
+ if (mListener != listener) {
+ throw new IllegalArgumentException("Attempting to unregister the wrong listener");
+ }
+ mListener = 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/mvcframework/LoaderActivity.java b/src/com/android/contacts/mvcframework/LoaderActivity.java
new file mode 100644
index 0000000..0e74d62
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/LoaderActivity.java
@@ -0,0 +1,144 @@
+/*
+ * 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.os.Bundle;
+
+import java.util.HashMap;
+
+/**
+ * The idea here was to abstract the generic life cycle junk needed to properly keep loaders going.
+ * It didn't work out as-is because registering the callbacks post config change didn't work.
+ */
+public abstract class LoaderActivity<D> extends Activity implements
+ Loader.OnLoadCompleteListener<D> {
+ static final class LoaderInfo {
+ public Bundle args;
+ public Loader loader;
+ }
+ private HashMap<Integer, LoaderInfo> mLoaders;
+
+ /**
+ * Registers a loader with this activity, registers the callbacks on it, and starts it loading.
+ */
+ protected void startLoading(int id, Bundle args) {
+ LoaderInfo info = mLoaders.get(id);
+ Loader loader;
+ if (info != null) {
+ loader = info.loader;
+ if (loader != null) {
+ loader.unregisterListener(this);
+ loader.destroy();
+ info.loader = null;
+ }
+ } else {
+ info = new LoaderInfo();
+ info.args = args;
+ }
+ mLoaders.put(id, info);
+ loader = onCreateLoader(id, args);
+ loader.registerListener(id, this);
+ loader.startLoading();
+ info.loader = loader;
+ }
+
+ protected abstract Loader onCreateLoader(int id, Bundle args);
+ protected abstract void onInitializeLoaders();
+
+ public abstract void onLoadComplete(int id, D data);
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+
+ if (mLoaders == null) {
+ // Look for a passed along loader and create a new one if it's not there
+ mLoaders = (HashMap<Integer, LoaderInfo>) getLastNonConfigurationInstance();
+ if (mLoaders == null) {
+ mLoaders = new HashMap<Integer, LoaderInfo>();
+ onInitializeLoaders();
+ }
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ // Call out to sub classes so they can start their loaders
+ // Let the existing loaders know that we want to be notified when a load is complete
+ for (HashMap.Entry<Integer, LoaderInfo> entry : mLoaders.entrySet()) {
+ LoaderInfo info = entry.getValue();
+ Loader loader = info.loader;
+ int id = entry.getKey();
+ if (loader == null) {
+ loader = onCreateLoader(id, info.args);
+ info.loader = loader;
+ } else {
+ loader.registerListener(id, this);
+ }
+ loader.startLoading();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ for (HashMap.Entry<Integer, LoaderInfo> entry : mLoaders.entrySet()) {
+ LoaderInfo info = entry.getValue();
+ Loader loader = info.loader;
+ if (loader == null) {
+ continue;
+ }
+
+ // Let the loader know we're done with it
+ loader.unregisterListener(this);
+
+ // The loader isn't getting passed along to the next instance so ask it to stop loading
+// if (!isChangingConfigurations()) {
+// loader.stopLoading();
+// }
+ }
+ }
+
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ // Pass the loader along to the next guy
+ Object result = mLoaders;
+ mLoaders = null;
+ return result;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (mLoaders != null) {
+ for (HashMap.Entry<Integer, LoaderInfo> entry : mLoaders.entrySet()) {
+ LoaderInfo info = entry.getValue();
+ Loader loader = info.loader;
+ if (loader == null) {
+ continue;
+ }
+ loader.destroy();
+ }
+ }
+ }
+}
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..424c63a
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactDetailView.java
@@ -0,0 +1,1022 @@
+/*
+ * 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 viewCache;
+
+ // Check to see if we can reuse convertView
+ if (convertView != null) {
+ v = convertView;
+ viewCache = (ViewCache) v.getTag();
+ } else {
+ // Create a new view if needed
+ v = mInflater.inflate(R.layout.list_item_text_icons, parent, false);
+
+ // Cache the children
+ viewCache = new ViewCache();
+ viewCache.label = (TextView) v.findViewById(android.R.id.text1);
+ viewCache.data = (TextView) v.findViewById(android.R.id.text2);
+ viewCache.footer = (TextView) v.findViewById(R.id.footer);
+ viewCache.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
+ viewCache.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon);
+ viewCache.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
+ viewCache.secondaryActionButton = (ImageView) v.findViewById(
+ R.id.secondary_action_button);
+ viewCache.secondaryActionButton.setOnClickListener(this);
+ viewCache.secondaryActionDivider = v.findViewById(R.id.divider);
+ v.setTag(viewCache);
+ }
+
+ // Update the entry in the view cache
+ viewCache.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);
+ 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) {
+ if (mCallbacks == null) return;
+ if (v == null) return;
+ final ViewEntry entry = (ViewEntry) v.getTag();
+ if (entry == null) return;
+ mCallbacks.onSecondaryClick(entry);
+ }
+ }
+
+ 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) {
+ if (mCallbacks == null) return;
+ final ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
+ if (entry == null) return;
+ mCallbacks.onPrimaryClick(entry);
+ }
+
+ 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..103136a
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactLoader.java
@@ -0,0 +1,390 @@
+/*
+ * 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;
+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.Result> {
+ 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;
+ }
+
+ public final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+
+ /**
+ * Used for synchronuous calls in unit test
+ * @hide
+ */
+ public Result testExecute() {
+ return doInBackground();
+ }
+
+ @Override
+ protected Result doInBackground(Void... args) {
+ final ContentResolver resolver = getContext().getContentResolver();
+ Uri uriCurrentFormat = convertLegacyIfNecessary(mLookupUri);
+ Result result = loadContactHeaderData(resolver, uriCurrentFormat);
+ 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, uriCurrentFormat);
+ 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;
+ }
+
+ /**
+ * Transforms the given Uri and returns a Lookup-Uri that represents the contact.
+ * For legacy contacts, a raw-contact lookup is performed.
+ */
+ private Uri convertLegacyIfNecessary(Uri uri) {
+ if (uri == null) throw new IllegalArgumentException("uri must not be null");
+
+ final String authority = uri.getAuthority();
+
+ // Current Style Uri? Just return it
+ if (ContactsContract.AUTHORITY.equals(authority)) {
+ return uri;
+ }
+
+ // Legacy Style? Convert to RawContact
+ final String OBSOLETE_AUTHORITY = "contacts";
+ if (OBSOLETE_AUTHORITY.equals(authority)) {
+ // Legacy Format. Convert to RawContact-Uri and then lookup the contact
+ final long rawContactId = ContentUris.parseId(uri);
+ return RawContacts.getContactLookupUri(getContext().getContentResolver(),
+ ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+ }
+
+ throw new IllegalArgumentException("uri format is unknown");
+ }
+
+ /**
+ * 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 further updates
+ if (mDestroyed) {
+ return;
+ }
+
+ mContact = result;
+ if (result != null) {
+ if (mObserver == null) {
+ mObserver = new ForceLoadContentObserver();
+ }
+ Log.i(TAG, "Registering content observer for " + mLookupUri);
+ getContext().getContentResolver().registerContentObserver(
+ mLookupUri, true, mObserver);
+ deliverResult(result);
+ }
+ }
+ }
+
+ public ContactLoader(Context context, Uri lookupUri) {
+ super(context);
+ mLookupUri = lookupUri;
+ }
+
+ @Override
+ public void startLoading() {
+ if (mContact != null) {
+ deliverResult(mContact);
+ } else {
+ forceLoad();
+ }
+ }
+
+ @Override
+ public void forceLoad() {
+ new LoadContactTask().execute((Void[])null);
+ }
+
+ @Override
+ public void stopLoading() {
+ mContact = null;
+ if (mObserver != null) {
+ getContext().getContentResolver().unregisterContentObserver(mObserver);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ mContact = null;
+ mDestroyed = true;
+ }
+}
diff --git a/tests/src/com/android/contacts/ContactDetailLoaderTest.java b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
new file mode 100644
index 0000000..f7a7a84
--- /dev/null
+++ b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
@@ -0,0 +1,296 @@
+/*
+ * 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 com.android.contacts.views.detail.ContactLoader;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.test.AndroidTestCase;
+
+import junit.framework.AssertionFailedError;
+
+/**
+ * Runs ContactLoader tests for the the contact-detail view.
+ * TODO: Warning: This currently only works on wiped phones as this will wipe
+ * your contact data
+ * TODO: Test all fields returned by the Loader
+ * TODO: Test social entries returned by the Loader
+ */
+public class ContactDetailLoaderTest extends AndroidTestCase {
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ //mContext.getContentResolver().delete(Data.CONTENT_URI, null, null);
+ //mContext.getContentResolver().delete(RawContacts.CONTENT_URI, null, null);
+ }
+
+ /**
+ * Utility function to ensure that an Exception is thrown during the code
+ * TODO: This should go to MoreAsserts at one point
+ */
+ @SuppressWarnings("unchecked")
+ private static <E extends Throwable> E assertThrows(
+ Class<E> expectedException, Runnable runnable) {
+ try {
+ runnable.run();
+ } catch (Throwable exception) {
+ Class<? extends Throwable> receivedException = exception.getClass();
+ if (expectedException == receivedException) return (E) exception;
+ throw new AssertionFailedError("Expected Exception " + expectedException +
+ " but " + receivedException + " was thrown. Details: " + exception);
+ }
+ throw new AssertionFailedError(
+ "Expected Exception " + expectedException + " which was not thrown");
+ }
+
+ private ContactLoader.Result assertLoadContact(Uri uri) {
+ final ContactLoader loader = new ContactLoader(mContext, uri);
+ final ContactLoader.LoadContactTask loadContactTask = loader.new LoadContactTask();
+ return loadContactTask.testExecute();
+ }
+
+ protected Uri insertStructuredName(long rawContactId, String givenName, String familyName) {
+ ContentValues values = new ContentValues();
+ StringBuilder sb = new StringBuilder();
+ if (givenName != null) {
+ sb.append(givenName);
+ }
+ if (givenName != null && familyName != null) {
+ sb.append(" ");
+ }
+ if (familyName != null) {
+ sb.append(familyName);
+ }
+ values.put(StructuredName.DISPLAY_NAME, sb.toString());
+ values.put(StructuredName.GIVEN_NAME, givenName);
+ values.put(StructuredName.FAMILY_NAME, familyName);
+
+ return insertStructuredName(rawContactId, values);
+ }
+
+ protected Uri insertStructuredName(long rawContactId, ContentValues values) {
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ Uri resultUri = getContext().getContentResolver().insert(Data.CONTENT_URI, values);
+ return resultUri;
+ }
+
+ protected Cursor queryRawContact(long rawContactId) {
+ return getContext().getContentResolver().query(ContentUris.withAppendedId(
+ RawContacts.CONTENT_URI, rawContactId), null, null, null, null);
+ }
+
+ protected Cursor queryContact(long contactId) {
+ return getContext().getContentResolver().query(ContentUris.withAppendedId(
+ Contacts.CONTENT_URI, contactId), null, null, null, null);
+ }
+
+ private long getContactIdByRawContactId(long rawContactId) {
+ Cursor c = queryRawContact(rawContactId);
+ assertTrue(c.moveToFirst());
+ long contactId = c.getLong(c.getColumnIndex(RawContacts.CONTACT_ID));
+ c.close();
+ return contactId;
+ }
+
+ private String getContactLookupByContactId(long contactId) {
+ Cursor c = queryContact(contactId);
+ assertTrue(c.moveToFirst());
+ String lookup = c.getString(c.getColumnIndex(Contacts.LOOKUP_KEY));
+ c.close();
+ return lookup;
+ }
+
+ public long createRawContact(String sourceId, String givenName, String familyName) {
+ ContentValues values = new ContentValues();
+
+ values.put(RawContacts.ACCOUNT_NAME, "aa");
+ values.put(RawContacts.ACCOUNT_TYPE, "mock");
+ values.put(RawContacts.SOURCE_ID, sourceId);
+ values.put(RawContacts.VERSION, 1);
+ values.put(RawContacts.DELETED, 0);
+ values.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
+ values.put(RawContacts.CUSTOM_RINGTONE, "d");
+ values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+ values.put(RawContacts.LAST_TIME_CONTACTED, 12345);
+ values.put(RawContacts.STARRED, 1);
+ values.put(RawContacts.SYNC1, "e");
+ values.put(RawContacts.SYNC2, "f");
+ values.put(RawContacts.SYNC3, "g");
+ values.put(RawContacts.SYNC4, "h");
+
+ Uri rawContactUri =
+ getContext().getContentResolver().insert(RawContacts.CONTENT_URI, values);
+
+ long rawContactId = ContentUris.parseId(rawContactUri);
+ insertStructuredName(rawContactId, givenName, familyName);
+ return rawContactId;
+ }
+
+ public void testNullUri() {
+ IllegalArgumentException e =
+ assertThrows(IllegalArgumentException.class, new Runnable() {
+ public void run() {
+ assertLoadContact(null);
+ }
+ });
+ assertEquals(e.getMessage(), "uri must not be null");
+ }
+
+ public void testEmptyUri() {
+ IllegalArgumentException e =
+ assertThrows(IllegalArgumentException.class, new Runnable() {
+ public void run() {
+ assertLoadContact(Uri.EMPTY);
+ }
+ });
+ assertEquals(e.getMessage(), "uri format is unknown");
+ }
+
+ public void testInvalidUri() {
+ IllegalArgumentException e =
+ assertThrows(IllegalArgumentException.class, new Runnable() {
+ public void run() {
+ assertLoadContact(Uri.parse("content://wtf"));
+ }
+ });
+ assertEquals(e.getMessage(), "uri format is unknown");
+ }
+
+ public void testLoadContactWithContactIdUri() {
+ // Use content Uris that only contain the ID
+ // Use some special characters in the source id to ensure that Encode/Decode properly
+ // works in Uris
+ long rawContactId1 = createRawContact("JohnDoe:;\"'[]{}=+-_\\|/.,<>?!@#$", "John", "Doe");
+ long rawContactId2 = createRawContact("JaneDuh%12%%^&*()", "Jane", "Duh");
+
+ long contactId1 = getContactIdByRawContactId(rawContactId1);
+ long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+ Uri contactUri1 = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId1);
+ Uri contactUri2 = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId2);
+
+ ContactLoader.Result contact1 = assertLoadContact(contactUri1);
+ ContactLoader.Result contact2 = assertLoadContact(contactUri2);
+
+ assertEquals(contactId1, contact1.getId());
+ assertEquals(contactId2, contact2.getId());
+ }
+
+ public void testLoadContactWithOldStyleUri() {
+ // Use content Uris that only contain the ID but use the format used in Donut
+ long rawContactId1 = createRawContact("JohnDoe", "John", "Doe");
+ long rawContactId2 = createRawContact("JaneDuh", "Jane", "Duh");
+
+ Uri oldUri1 = ContentUris.withAppendedId(Uri.parse("content://contacts"), rawContactId1);
+ Uri oldUri2 = ContentUris.withAppendedId(Uri.parse("content://contacts"), rawContactId2);
+
+ ContactLoader.Result contact1 = assertLoadContact(oldUri1);
+ ContactLoader.Result contact2 = assertLoadContact(oldUri2);
+
+ long contactId1 = getContactIdByRawContactId(rawContactId1);
+ long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+ assertEquals(contactId1, contact1.getId());
+ assertEquals(contactId2, contact2.getId());
+ }
+
+ public void testLoadContactWithContactLookupUri() {
+ // Use lookup-style Uris that do not contain the Contact-ID
+ long rawContactId1 = createRawContact("JohnDoe", "John", "Doe");
+ long rawContactId2 = createRawContact("JaneDuh", "Jane", "Duh");
+
+ assertTrue(rawContactId1 != rawContactId2);
+
+ long contactId1 = getContactIdByRawContactId(rawContactId1);
+ long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+ assertTrue(contactId1 != contactId2);
+
+ String lookupKey1 = getContactLookupByContactId(contactId1);
+ String lookupKey2 = getContactLookupByContactId(contactId2);
+ assertFalse(lookupKey1.equals(lookupKey2));
+
+ Uri contactLookupUri1 = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey1);
+ Uri contactLookupUri2 = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey2);
+
+ ContactLoader.Result contact1 = assertLoadContact(contactLookupUri1);
+ ContactLoader.Result contact2 = assertLoadContact(contactLookupUri2);
+
+ assertEquals(contactId1, contact1.getId());
+ assertEquals(contactId2, contact2.getId());
+ }
+
+ public void testLoadContactWithContactLookupAndIdUri() {
+ // Use lookup-style Uris that also contain the Contact-ID
+ long rawContactId1 = createRawContact("JohnDoe", "John", "Doe");
+ long rawContactId2 = createRawContact("JaneDuh", "Jane", "Duh");
+
+ long contactId1 = getContactIdByRawContactId(rawContactId1);
+ long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+ String lookupKey1 = getContactLookupByContactId(contactId1);
+ String lookupKey2 = getContactLookupByContactId(contactId2);
+
+ Uri contactLookupUri1 = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey1), contactId1);
+ Uri contactLookupUri2 = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey2), contactId2);
+
+ ContactLoader.Result contact1 = assertLoadContact(contactLookupUri1);
+ ContactLoader.Result contact2 = assertLoadContact(contactLookupUri2);
+
+ assertEquals(contactId1, contact1.getId());
+ assertEquals(contactId2, contact2.getId());
+ }
+
+ public void testLoadContactWithContactLookupWithIncorrectIdUri() {
+ // Use lookup-style Uris that contain incorrect Contact-ID
+ // (we want to ensure that still the correct contact is chosen)
+
+ long rawContactId1 = createRawContact("JohnDoe", "John", "Doe");
+ long rawContactId2 = createRawContact("JaneDuh", "Jane", "Duh");
+
+ long contactId1 = getContactIdByRawContactId(rawContactId1);
+ long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+ String lookupKey1 = getContactLookupByContactId(contactId1);
+ String lookupKey2 = getContactLookupByContactId(contactId2);
+
+ long[] fakeIds = new long[] { 0, rawContactId1, rawContactId2, contactId1, contactId2 };
+
+ for (long fakeContactId : fakeIds) {
+ Uri contactLookupUri1 = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey1), fakeContactId);
+ Uri contactLookupUri2 = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey2), fakeContactId);
+
+ ContactLoader.Result contact1 = assertLoadContact(contactLookupUri1);
+ ContactLoader.Result contact2 = assertLoadContact(contactLookupUri2);
+
+ assertEquals(contactId1, contact1.getId());
+ assertEquals(contactId2, contact2.getId());
+ }
+ }
+}
diff --git a/tests/src/com/android/contacts/ContactListModeTest.java b/tests/src/com/android/contacts/ContactListModeTest.java
new file mode 100644
index 0000000..fe8ef5c
--- /dev/null
+++ b/tests/src/com/android/contacts/ContactListModeTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts;
+
+import com.android.contacts.tests.mocks.ContactsMockContext;
+import com.android.contacts.tests.mocks.MockContentProvider;
+
+import android.content.Intent;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.ProviderStatus;
+import android.provider.ContactsContract.StatusUpdates;
+import android.test.ActivityUnitTestCase;
+
+/**
+ * Tests for the contact list activity modes.
+ *
+ * Running all tests:
+ *
+ * runtest contacts
+ * or
+ * adb shell am instrument \
+ * -w com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+public class ContactListModeTest
+ extends ActivityUnitTestCase<ContactsListActivity> {
+
+ private ContactsMockContext mContext;
+ private MockContentProvider mContactsProvider;
+ private MockContentProvider mSettingsProvider;
+
+ public ContactListModeTest() {
+ super(ContactsListActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mContext = new ContactsMockContext(getInstrumentation().getTargetContext());
+ mContactsProvider = mContext.getContactsProvider();
+ mSettingsProvider = mContext.getSettingsProvider();
+ setActivityContext(mContext);
+ }
+
+ public void testDefaultMode() throws Exception {
+ mContactsProvider.expectQuery(ProviderStatus.CONTENT_URI)
+ .withProjection(ProviderStatus.STATUS, ProviderStatus.DATA1);
+
+ mSettingsProvider.expectQuery(Settings.System.CONTENT_URI)
+ .withProjection(Settings.System.VALUE)
+ .withSelection(Settings.System.NAME + "=?",
+ ContactsContract.Preferences.SORT_ORDER);
+
+ mSettingsProvider.expectQuery(Settings.System.CONTENT_URI)
+ .withProjection(Settings.System.VALUE)
+ .withSelection(Settings.System.NAME + "=?",
+ ContactsContract.Preferences.DISPLAY_ORDER);
+
+ mContactsProvider.expectQuery(
+ Contacts.CONTENT_URI.buildUpon()
+ .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true")
+ .build())
+ .withProjection(
+ Contacts._ID,
+ Contacts.DISPLAY_NAME,
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ Contacts.SORT_KEY_PRIMARY,
+ Contacts.STARRED,
+ Contacts.TIMES_CONTACTED,
+ Contacts.CONTACT_PRESENCE,
+ Contacts.PHOTO_ID,
+ Contacts.LOOKUP_KEY,
+ Contacts.PHONETIC_NAME,
+ Contacts.HAS_PHONE_NUMBER)
+ .withSelection(Contacts.IN_VISIBLE_GROUP + "=1")
+ .withSortOrder(Contacts.SORT_KEY_PRIMARY)
+ .returnRow(1, "John", "John", "john", 1, 10,
+ StatusUpdates.AVAILABLE, 23, "lk1", "john", 1)
+ .returnRow(2, "Jim", "Jim", "jim", 1, 8,
+ StatusUpdates.AWAY, 24, "lk2", "jim", 0);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, ContactsContract.Contacts.CONTENT_URI);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent, null, null);
+ ContactsListActivity activity = getActivity();
+ activity.runQueriesSynchronously();
+ activity.onResume(); // Trigger the queries
+
+ assertEquals(3, activity.getListAdapter().getCount());
+ }
+}
diff --git a/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
new file mode 100644
index 0000000..bd2010e
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
@@ -0,0 +1,58 @@
+/*
+ * 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.tests.mocks;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
+
+/**
+ * A mock context for contact activity unit tests. Forwards everything to
+ * a supplied context, except content resolver operations, which are sent
+ * to mock content providers.
+ */
+public class ContactsMockContext extends ContextWrapper {
+
+ private MockContentResolver mContentResolver;
+ private MockContentProvider mContactsProvider;
+ private MockContentProvider mSettingsProvider;
+
+ public ContactsMockContext(Context base) {
+ super(base);
+ mContentResolver = new MockContentResolver();
+ mContactsProvider = new MockContentProvider();
+ mContentResolver.addProvider(ContactsContract.AUTHORITY, mContactsProvider);
+ mSettingsProvider = new MockContentProvider();
+ mContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider);
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mContentResolver;
+ }
+
+ public MockContentProvider getContactsProvider() {
+ return mContactsProvider;
+ }
+
+ public MockContentProvider getSettingsProvider() {
+ return mSettingsProvider;
+ }
+}
diff --git a/tests/src/com/android/contacts/tests/mocks/MockContentProvider.java b/tests/src/com/android/contacts/tests/mocks/MockContentProvider.java
new file mode 100644
index 0000000..63b134a
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/mocks/MockContentProvider.java
@@ -0,0 +1,207 @@
+/*
+ * 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.tests.mocks;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+
+import junit.framework.Assert;
+
+/**
+ * A programmable mock content provider.
+ */
+public class MockContentProvider extends ContentProvider {
+
+ public static class Query {
+
+ private final Uri mUri;
+ private String[] mProjection;
+ private String[] mDefaultProjection;
+ private String mSelection;
+ private String[] mSelectionArgs;
+ private String mSortOrder;
+ private ArrayList<Object[]> mRows = new ArrayList<Object[]>();
+
+ public Query(Uri uri) {
+ mUri = uri;
+ }
+
+ @Override
+ public String toString() {
+ return queryToString(mUri, mProjection, mSelection, mSelectionArgs, mSortOrder);
+ }
+
+ public Query withProjection(String... projection) {
+ mProjection = projection;
+ return this;
+ }
+
+ public Query withDefaultProjection(String... projection) {
+ mDefaultProjection = projection;
+ return this;
+ }
+
+ public Query withSelection(String selection, String... selectionArgs) {
+ mSelection = selection;
+ mSelectionArgs = selectionArgs;
+ return this;
+ }
+
+ public Query withSortOrder(String sortOrder) {
+ mSortOrder = sortOrder;
+ return this;
+ }
+
+ public Query returnRow(Object... row) {
+ mRows.add(row);
+ return this;
+ }
+
+ public boolean equals(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ if (!uri.equals(mUri)) {
+ return false;
+ }
+
+ if (!equals(projection, mProjection)) {
+ return false;
+ }
+
+ if (!TextUtils.equals(selection, mSelection)) {
+ return false;
+ }
+
+ if (!equals(selectionArgs, mSelectionArgs)) {
+ return false;
+ }
+
+ if (!TextUtils.equals(sortOrder, mSortOrder)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean equals(String[] array1, String[] array2) {
+ boolean empty1 = array1 == null || array1.length == 0;
+ boolean empty2 = array2 == null || array2.length == 0;
+ if (empty1 && empty2) {
+ return true;
+ }
+ if (empty1) {
+ return false;
+ }
+
+ for (int i = 0; i < array1.length; i++) {
+ if (!array1[i].equals(array2[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public Cursor getResult() {
+ String[] columnNames = mProjection != null ? mProjection : mDefaultProjection;
+ MatrixCursor cursor = new MatrixCursor(columnNames);
+ for (Object[] row : mRows) {
+ cursor.addRow(row);
+ }
+ return cursor;
+ }
+ }
+
+ private LinkedList<Query> mExpectedQueries = new LinkedList<Query>();
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ public Query expectQuery(Uri contentUri) {
+ Query query = new Query(contentUri);
+ mExpectedQueries.offer(query);
+ return query;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ if (mExpectedQueries.isEmpty()) {
+ Assert.fail("Unexpected query: "
+ + queryToString(uri, projection, selection, selectionArgs, sortOrder));
+ }
+
+ Query query = mExpectedQueries.remove();
+ if (!query.equals(uri, projection, selection, selectionArgs, sortOrder)) {
+ Assert.fail("Incorrect query.\n Expected: " + query + "\n Actual: " +
+ queryToString(uri, projection, selection, selectionArgs, sortOrder));
+ }
+
+ return query.getResult();
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ private static String queryToString(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(uri).append(" ");
+ if (projection != null) {
+ sb.append(Arrays.toString(projection));
+ } else {
+ sb.append("[]");
+ }
+ if (selection != null) {
+ sb.append(" selection: '").append(selection).append("'");
+ if (projection != null) {
+ sb.append(Arrays.toString(selectionArgs));
+ } else {
+ sb.append("[]");
+ }
+ }
+ if (sortOrder != null) {
+ sb.append(" sort: '").append(sortOrder).append("'");
+ }
+ return sb.toString();
+ }
+}