am ffa7918a: am 15a5ad8e: Give the android core shared user a label.
Merge commit 'ffa7918aa988e4c819a73c9d491967b9fa1a45cf'
* commit 'ffa7918aa988e4c819a73c9d491967b9fa1a45cf':
Give the android core shared user a label.
diff --git a/Android.mk b/Android.mk
index d574b3f..e7624f7 100644
--- a/Android.mk
+++ b/Android.mk
@@ -5,7 +5,7 @@
LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_STATIC_JAVA_LIBRARIES := com.android.phone.common
+LOCAL_STATIC_JAVA_LIBRARIES := com.android.phone.common com.android.vcard
LOCAL_PACKAGE_NAME := Contacts
LOCAL_CERTIFICATE := shared
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 09bdb1d..6215870 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -240,24 +240,32 @@
<data android:mimeType="vnd.android.cursor.item/postal-address_v2" android:host="com.android.contacts" />
<data android:mimeType="vnd.android.cursor.item/postal-address" android:host="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>
+ <!-- An activity for selecting multiple phone numbers -->
+ <activity android:name="MultiplePhonePickerActivity">
+ <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>
+
<!-- The contacts search/filter UI -->
- <activity android:name="ContactsListActivity$ContactsSearchActivity"
+ <activity android:name="ContactsSearchActivity"
android:theme="@style/ContactsSearchTheme"
- android:windowSoftInputMode="stateAlwaysVisible|adjustPan"
+ android:windowSoftInputMode="stateAlwaysVisible"
>
<intent-filter>
<action android:name="com.android.contacts.action.FILTER_CONTACTS" />
@@ -364,7 +372,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">
@@ -377,10 +385,24 @@
</intent-filter>
</activity>
- <!-- Edit or insert details for a contact -->
+ <!-- Shows the List and Details in two panes. This is probably temporary -->
+ <activity android:name=".activities.TwoPaneActivity"
+ android:label="Contacts Goop"
+ android:theme="@style/TallTitleBarTheme">
+
+ <intent-filter android:label="Contacts Goop">
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ </intent-filter>
+ </activity>
+
+ <!-- Edit an existing contact -->
<activity
- android:name=".ui.EditContactActivity"
- android:windowSoftInputMode="stateHidden|adjustResize">
+ android:name=".activities.ContactEditorActivity"
+ android:theme="@style/TallTitleBarTheme"
+ android:windowSoftInputMode="adjustResize">
<intent-filter android:label="@string/editContactDescription">
<action android:name="android.intent.action.EDIT" />
@@ -389,6 +411,12 @@
<data android:mimeType="vnd.android.cursor.item/contact" android:host="com.android.contacts" />
<data android:mimeType="vnd.android.cursor.item/raw_contact" android:host="com.android.contacts" />
</intent-filter>
+ </activity>
+
+ <!-- Create contact (for now this is seperate) -->
+ <activity
+ android:name=".ui.EditContactActivity"
+ android:theme="@style/TallTitleBarTheme">
<intent-filter android:label="@string/insertContactDescription">
<action android:name="android.intent.action.INSERT" />
@@ -397,7 +425,6 @@
<data android:mimeType="vnd.android.cursor.dir/contact" />
<data android:mimeType="vnd.android.cursor.dir/raw_contact" />
</intent-filter>
-
</activity>
<!-- Stub service used to keep our process alive long enough for
@@ -406,6 +433,11 @@
android:name=".util.EmptyService"
android:exported="false" />
+ <!-- Service to save a contact -->
+ <service
+ android:name=".views.ContactSaveService"
+ android:exported="false" />
+
<!-- Views the details of a single contact -->
<activity android:name="ContactOptionsActivity"
android:label="@string/contactOptionsTitle"
@@ -428,6 +460,11 @@
/>
</activity>
+ <!-- Interstitial activity that shows a phone disambig dialog -->
+ <activity android:name="CallContactActivity"
+ android:theme="@android:style/Theme.Translucent">
+ </activity>
+
<!-- Makes .ContactsListActivity the search target for any activity in Contacts -->
<meta-data
android:name="android.app.default_searchable"
@@ -465,17 +502,34 @@
</intent-filter>
</activity>
- <activity android:name=".ImportVCardActivity"
+ <!-- vCard related -->
+ <activity android:name=".vcard.ImportVCardActivity"
android:theme="@style/BackgroundOnly">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="text/directory" />
+ <data android:mimeType="text/vcard" />
<data android:mimeType="text/x-vcard" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
- <activity android:name=".ExportVCardActivity"
+ <activity android:name=".vcard.SelectAccountActivity"
android:theme="@style/BackgroundOnly" />
+
+ <activity android:name=".vcard.ExportVCardActivity"
+ android:theme="@style/BackgroundOnly" />
+
+ <service
+ android:name=".vcard.VCardService"
+ android:exported="false" />
+
+ <!-- Pinned header list demo -->
+ <activity android:name=".widget.PinnedHeaderListDemoActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
</application>
</manifest>
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-finger/btn_dial_textfield.xml b/res/drawable-finger/btn_dial_textfield.xml
deleted file mode 100644
index 109f6ae..0000000
--- a/res/drawable-finger/btn_dial_textfield.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_pressed="true"
- android:drawable="@drawable/btn_dial_textfield_pressed" />
- <item android:state_focused="true"
- android:drawable="@drawable/btn_dial_textfield_normal" />
- <item
- android:drawable="@drawable/btn_dial_textfield_normal" />
-</selector>
-
diff --git a/res/drawable-finger/btn_dial_textfield_active.xml b/res/drawable-finger/btn_dial_textfield_active.xml
deleted file mode 100644
index 246d543..0000000
--- a/res/drawable-finger/btn_dial_textfield_active.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_pressed="true"
- android:drawable="@drawable/btn_dial_textfield_pressed" />
- <item android:state_focused="true"
- android:drawable="@drawable/btn_dial_textfield_activated" />
- <item
- android:drawable="@drawable/btn_dial_textfield_activated" />
-</selector>
-
diff --git a/res/drawable-hdpi/aizy_bottom.png b/res/drawable-hdpi/aizy_bottom.png
new file mode 100644
index 0000000..1f3d332
--- /dev/null
+++ b/res/drawable-hdpi/aizy_bottom.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-hdpi-finger/badge_action_call.png b/res/drawable-hdpi/badge_action_call.png
similarity index 100%
rename from res/drawable-hdpi-finger/badge_action_call.png
rename to res/drawable-hdpi/badge_action_call.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/badge_action_sms.png b/res/drawable-hdpi/badge_action_sms.png
similarity index 100%
rename from res/drawable-hdpi-finger/badge_action_sms.png
rename to res/drawable-hdpi/badge_action_sms.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/bg_blk_search_contact.9.png b/res/drawable-hdpi/bg_blk_search_contact.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/bg_blk_search_contact.9.png
rename to res/drawable-hdpi/bg_blk_search_contact.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_circle_disable.png b/res/drawable-hdpi/btn_circle_disable.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_circle_disable.png
rename to res/drawable-hdpi/btn_circle_disable.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_circle_disable_focused.png b/res/drawable-hdpi/btn_circle_disable_focused.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_circle_disable_focused.png
rename to res/drawable-hdpi/btn_circle_disable_focused.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_circle_normal.png b/res/drawable-hdpi/btn_circle_normal.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_circle_normal.png
rename to res/drawable-hdpi/btn_circle_normal.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_circle_pressed.png b/res/drawable-hdpi/btn_circle_pressed.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_circle_pressed.png
rename to res/drawable-hdpi/btn_circle_pressed.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_circle_selected.png b/res/drawable-hdpi/btn_circle_selected.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_circle_selected.png
rename to res/drawable-hdpi/btn_circle_selected.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_left_disable.9.png b/res/drawable-hdpi/btn_dial_action_left_disable.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_left_disable.9.png
rename to res/drawable-hdpi/btn_dial_action_left_disable.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_left_disable_focused.9.png b/res/drawable-hdpi/btn_dial_action_left_disable_focused.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_left_disable_focused.9.png
rename to res/drawable-hdpi/btn_dial_action_left_disable_focused.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_left_normal.9.png b/res/drawable-hdpi/btn_dial_action_left_normal.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_left_normal.9.png
rename to res/drawable-hdpi/btn_dial_action_left_normal.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_left_pressed.9.png b/res/drawable-hdpi/btn_dial_action_left_pressed.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_left_pressed.9.png
rename to res/drawable-hdpi/btn_dial_action_left_pressed.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_left_selected.9.png b/res/drawable-hdpi/btn_dial_action_left_selected.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_left_selected.9.png
rename to res/drawable-hdpi/btn_dial_action_left_selected.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_middle_disable.9.png b/res/drawable-hdpi/btn_dial_action_middle_disable.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_middle_disable.9.png
rename to res/drawable-hdpi/btn_dial_action_middle_disable.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_middle_disable_focused.9.png b/res/drawable-hdpi/btn_dial_action_middle_disable_focused.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_middle_disable_focused.9.png
rename to res/drawable-hdpi/btn_dial_action_middle_disable_focused.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_middle_normal.9.png b/res/drawable-hdpi/btn_dial_action_middle_normal.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_middle_normal.9.png
rename to res/drawable-hdpi/btn_dial_action_middle_normal.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_middle_pressed.9.png b/res/drawable-hdpi/btn_dial_action_middle_pressed.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_middle_pressed.9.png
rename to res/drawable-hdpi/btn_dial_action_middle_pressed.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_middle_selected.9.png b/res/drawable-hdpi/btn_dial_action_middle_selected.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_middle_selected.9.png
rename to res/drawable-hdpi/btn_dial_action_middle_selected.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_right_disable.9.png b/res/drawable-hdpi/btn_dial_action_right_disable.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_right_disable.9.png
rename to res/drawable-hdpi/btn_dial_action_right_disable.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_right_disable_focused.9.png b/res/drawable-hdpi/btn_dial_action_right_disable_focused.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_right_disable_focused.9.png
rename to res/drawable-hdpi/btn_dial_action_right_disable_focused.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_right_normal.9.png b/res/drawable-hdpi/btn_dial_action_right_normal.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_right_normal.9.png
rename to res/drawable-hdpi/btn_dial_action_right_normal.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_right_pressed.9.png b/res/drawable-hdpi/btn_dial_action_right_pressed.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_right_pressed.9.png
rename to res/drawable-hdpi/btn_dial_action_right_pressed.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_action_right_selected.9.png b/res/drawable-hdpi/btn_dial_action_right_selected.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_action_right_selected.9.png
rename to res/drawable-hdpi/btn_dial_action_right_selected.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_normal.9.png b/res/drawable-hdpi/btn_dial_normal.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_normal.9.png
rename to res/drawable-hdpi/btn_dial_normal.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_pressed.9.png b/res/drawable-hdpi/btn_dial_pressed.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_pressed.9.png
rename to res/drawable-hdpi/btn_dial_pressed.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_selected.9.png b/res/drawable-hdpi/btn_dial_selected.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_selected.9.png
rename to res/drawable-hdpi/btn_dial_selected.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_textfield_activated.9.png b/res/drawable-hdpi/btn_dial_textfield_activated.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_textfield_activated.9.png
rename to res/drawable-hdpi/btn_dial_textfield_activated.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_textfield_normal.9.png b/res/drawable-hdpi/btn_dial_textfield_normal.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_textfield_normal.9.png
rename to res/drawable-hdpi/btn_dial_textfield_normal.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_textfield_pressed.9.png b/res/drawable-hdpi/btn_dial_textfield_pressed.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_textfield_pressed.9.png
rename to res/drawable-hdpi/btn_dial_textfield_pressed.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_dial_textfield_selected.9.png b/res/drawable-hdpi/btn_dial_textfield_selected.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_dial_textfield_selected.9.png
rename to res/drawable-hdpi/btn_dial_textfield_selected.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_search_dialog_default.9.png b/res/drawable-hdpi/btn_search_dialog_default.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_search_dialog_default.9.png
rename to res/drawable-hdpi/btn_search_dialog_default.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_search_dialog_pressed.9.png b/res/drawable-hdpi/btn_search_dialog_pressed.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_search_dialog_pressed.9.png
rename to res/drawable-hdpi/btn_search_dialog_pressed.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_search_dialog_selected.9.png b/res/drawable-hdpi/btn_search_dialog_selected.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/btn_search_dialog_selected.9.png
rename to res/drawable-hdpi/btn_search_dialog_selected.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/contact_picture_border_highlight.9.png b/res/drawable-hdpi/contact_picture_border_highlight.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/contact_picture_border_highlight.9.png
rename to res/drawable-hdpi/contact_picture_border_highlight.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/contact_picture_border_normal.9.png b/res/drawable-hdpi/contact_picture_border_normal.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/contact_picture_border_normal.9.png
rename to res/drawable-hdpi/contact_picture_border_normal.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/contact_picture_border_pressed.9.png b/res/drawable-hdpi/contact_picture_border_pressed.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/contact_picture_border_pressed.9.png
rename to res/drawable-hdpi/contact_picture_border_pressed.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_0_blk.png b/res/drawable-hdpi/dial_num_0_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_0_blk.png
rename to res/drawable-hdpi/dial_num_0_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_0_wht.png b/res/drawable-hdpi/dial_num_0_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_0_wht.png
rename to res/drawable-hdpi/dial_num_0_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_1_no_vm_blk.png b/res/drawable-hdpi/dial_num_1_no_vm_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_1_no_vm_blk.png
rename to res/drawable-hdpi/dial_num_1_no_vm_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_1_no_vm_wht.png b/res/drawable-hdpi/dial_num_1_no_vm_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_1_no_vm_wht.png
rename to res/drawable-hdpi/dial_num_1_no_vm_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_2_blk.png b/res/drawable-hdpi/dial_num_2_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_2_blk.png
rename to res/drawable-hdpi/dial_num_2_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_2_wht.png b/res/drawable-hdpi/dial_num_2_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_2_wht.png
rename to res/drawable-hdpi/dial_num_2_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_3_blk.png b/res/drawable-hdpi/dial_num_3_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_3_blk.png
rename to res/drawable-hdpi/dial_num_3_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_3_wht.png b/res/drawable-hdpi/dial_num_3_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_3_wht.png
rename to res/drawable-hdpi/dial_num_3_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_4_blk.png b/res/drawable-hdpi/dial_num_4_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_4_blk.png
rename to res/drawable-hdpi/dial_num_4_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_4_wht.png b/res/drawable-hdpi/dial_num_4_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_4_wht.png
rename to res/drawable-hdpi/dial_num_4_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_5_blk.png b/res/drawable-hdpi/dial_num_5_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_5_blk.png
rename to res/drawable-hdpi/dial_num_5_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_5_wht.png b/res/drawable-hdpi/dial_num_5_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_5_wht.png
rename to res/drawable-hdpi/dial_num_5_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_6_blk.png b/res/drawable-hdpi/dial_num_6_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_6_blk.png
rename to res/drawable-hdpi/dial_num_6_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_6_wht.png b/res/drawable-hdpi/dial_num_6_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_6_wht.png
rename to res/drawable-hdpi/dial_num_6_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_7_blk.png b/res/drawable-hdpi/dial_num_7_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_7_blk.png
rename to res/drawable-hdpi/dial_num_7_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_7_wht.png b/res/drawable-hdpi/dial_num_7_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_7_wht.png
rename to res/drawable-hdpi/dial_num_7_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_8_blk.png b/res/drawable-hdpi/dial_num_8_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_8_blk.png
rename to res/drawable-hdpi/dial_num_8_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_8_wht.png b/res/drawable-hdpi/dial_num_8_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_8_wht.png
rename to res/drawable-hdpi/dial_num_8_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_9_blk.png b/res/drawable-hdpi/dial_num_9_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_9_blk.png
rename to res/drawable-hdpi/dial_num_9_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_9_wht.png b/res/drawable-hdpi/dial_num_9_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_9_wht.png
rename to res/drawable-hdpi/dial_num_9_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_pound_blk.png b/res/drawable-hdpi/dial_num_pound_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_pound_blk.png
rename to res/drawable-hdpi/dial_num_pound_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_pound_wht.png b/res/drawable-hdpi/dial_num_pound_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_pound_wht.png
rename to res/drawable-hdpi/dial_num_pound_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_star_blk.png b/res/drawable-hdpi/dial_num_star_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_star_blk.png
rename to res/drawable-hdpi/dial_num_star_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/dial_num_star_wht.png b/res/drawable-hdpi/dial_num_star_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/dial_num_star_wht.png
rename to res/drawable-hdpi/dial_num_star_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/divider_vertical_dark.9.png b/res/drawable-hdpi/divider_vertical_dark.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/divider_vertical_dark.9.png
rename to res/drawable-hdpi/divider_vertical_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/edit_focused.png b/res/drawable-hdpi/edit_focused.png
new file mode 100644
index 0000000..f2386dc
--- /dev/null
+++ b/res/drawable-hdpi/edit_focused.png
Binary files differ
diff --git a/res/drawable-hdpi/edit_normal.png b/res/drawable-hdpi/edit_normal.png
new file mode 100644
index 0000000..f33f841
--- /dev/null
+++ b/res/drawable-hdpi/edit_normal.png
Binary files differ
diff --git a/res/drawable-hdpi/edit_pressed.png b/res/drawable-hdpi/edit_pressed.png
new file mode 100644
index 0000000..e66927c
--- /dev/null
+++ b/res/drawable-hdpi/edit_pressed.png
Binary files differ
diff --git a/res/drawable-hdpi/edit_rawcontact_bg.png b/res/drawable-hdpi/edit_rawcontact_bg.png
new file mode 100644
index 0000000..84bd50b
--- /dev/null
+++ b/res/drawable-hdpi/edit_rawcontact_bg.png
Binary files differ
diff --git a/res/drawable-hdpi/edit_rawcontact_bottom.png b/res/drawable-hdpi/edit_rawcontact_bottom.png
new file mode 100644
index 0000000..58ae4b2
--- /dev/null
+++ b/res/drawable-hdpi/edit_rawcontact_bottom.png
Binary files differ
diff --git a/res/drawable-hdpi/edit_rawcontact_top.png b/res/drawable-hdpi/edit_rawcontact_top.png
new file mode 100644
index 0000000..6ca737d
--- /dev/null
+++ b/res/drawable-hdpi/edit_rawcontact_top.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_aggregated.png b/res/drawable-hdpi/ic_aggregated.png
new file mode 100644
index 0000000..7ca15b1
--- /dev/null
+++ b/res/drawable-hdpi/ic_aggregated.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_btn_round_less.png b/res/drawable-hdpi/ic_btn_round_less.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_btn_round_less.png
rename to res/drawable-hdpi/ic_btn_round_less.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_btn_round_minus.png b/res/drawable-hdpi/ic_btn_round_minus.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_btn_round_minus.png
rename to res/drawable-hdpi/ic_btn_round_minus.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_btn_round_more.png b/res/drawable-hdpi/ic_btn_round_more.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_btn_round_more.png
rename to res/drawable-hdpi/ic_btn_round_more.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_btn_round_plus.png b/res/drawable-hdpi/ic_btn_round_plus.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_btn_round_plus.png
rename to res/drawable-hdpi/ic_btn_round_plus.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_call_log_header_incoming_call.png b/res/drawable-hdpi/ic_call_log_header_incoming_call.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_call_log_header_incoming_call.png
rename to res/drawable-hdpi/ic_call_log_header_incoming_call.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_call_log_header_missed_call.png b/res/drawable-hdpi/ic_call_log_header_missed_call.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_call_log_header_missed_call.png
rename to res/drawable-hdpi/ic_call_log_header_missed_call.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_call_log_header_outgoing_call.png b/res/drawable-hdpi/ic_call_log_header_outgoing_call.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_call_log_header_outgoing_call.png
rename to res/drawable-hdpi/ic_call_log_header_outgoing_call.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_call_log_list_incoming_call.png b/res/drawable-hdpi/ic_call_log_list_incoming_call.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_call_log_list_incoming_call.png
rename to res/drawable-hdpi/ic_call_log_list_incoming_call.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_call_log_list_missed_call.png b/res/drawable-hdpi/ic_call_log_list_missed_call.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_call_log_list_missed_call.png
rename to res/drawable-hdpi/ic_call_log_list_missed_call.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_call_log_list_outgoing_call.png b/res/drawable-hdpi/ic_call_log_list_outgoing_call.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_call_log_list_outgoing_call.png
rename to res/drawable-hdpi/ic_call_log_list_outgoing_call.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_contact_list_picture.png b/res/drawable-hdpi/ic_contact_list_picture.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_contact_list_picture.png
rename to res/drawable-hdpi/ic_contact_list_picture.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_contact_picture.png b/res/drawable-hdpi/ic_contact_picture.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_contact_picture.png
rename to res/drawable-hdpi/ic_contact_picture.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_contact_picture_2.png b/res/drawable-hdpi/ic_contact_picture_2.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_contact_picture_2.png
rename to res/drawable-hdpi/ic_contact_picture_2.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_contact_picture_3.png b/res/drawable-hdpi/ic_contact_picture_3.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_contact_picture_3.png
rename to res/drawable-hdpi/ic_contact_picture_3.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_default_number.png b/res/drawable-hdpi/ic_default_number.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_default_number.png
rename to res/drawable-hdpi/ic_default_number.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_dial_action_call.png b/res/drawable-hdpi/ic_dial_action_call.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_dial_action_call.png
rename to res/drawable-hdpi/ic_dial_action_call.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_dial_action_delete.png b/res/drawable-hdpi/ic_dial_action_delete.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_dial_action_delete.png
rename to res/drawable-hdpi/ic_dial_action_delete.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_dial_action_voice_mail.png b/res/drawable-hdpi/ic_dial_action_voice_mail.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_dial_action_voice_mail.png
rename to res/drawable-hdpi/ic_dial_action_voice_mail.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_dial_number_blk.png b/res/drawable-hdpi/ic_dial_number_blk.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_dial_number_blk.png
rename to res/drawable-hdpi/ic_dial_number_blk.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_dial_number_wht.png b/res/drawable-hdpi/ic_dial_number_wht.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_dial_number_wht.png
rename to res/drawable-hdpi/ic_dial_number_wht.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_dialer_fork_add_call.png b/res/drawable-hdpi/ic_dialer_fork_add_call.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_dialer_fork_add_call.png
rename to res/drawable-hdpi/ic_dialer_fork_add_call.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_dialer_fork_current_call.png b/res/drawable-hdpi/ic_dialer_fork_current_call.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_dialer_fork_current_call.png
rename to res/drawable-hdpi/ic_dialer_fork_current_call.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_dialer_fork_tt_keypad.png b/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_dialer_fork_tt_keypad.png
rename to res/drawable-hdpi/ic_dialer_fork_tt_keypad.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_join.png b/res/drawable-hdpi/ic_join.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_join.png
rename to res/drawable-hdpi/ic_join.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_menu_account_list.png b/res/drawable-hdpi/ic_menu_account_list.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_menu_account_list.png
rename to res/drawable-hdpi/ic_menu_account_list.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_menu_add_picture.png b/res/drawable-hdpi/ic_menu_add_picture.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_menu_add_picture.png
rename to res/drawable-hdpi/ic_menu_add_picture.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_display_all.png b/res/drawable-hdpi/ic_menu_display_all.png
new file mode 100755
index 0000000..563083c
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_display_all.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_display_selected.png b/res/drawable-hdpi/ic_menu_display_selected.png
new file mode 100644
index 0000000..76b2e22
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_display_selected.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_menu_import_export.png b/res/drawable-hdpi/ic_menu_import_export.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_menu_import_export.png
rename to res/drawable-hdpi/ic_menu_import_export.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_menu_merge.png b/res/drawable-hdpi/ic_menu_merge.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_menu_merge.png
rename to res/drawable-hdpi/ic_menu_merge.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_select.png b/res/drawable-hdpi/ic_menu_select.png
new file mode 100644
index 0000000..c5bb503
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_select.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_menu_split.png b/res/drawable-hdpi/ic_menu_split.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_menu_split.png
rename to res/drawable-hdpi/ic_menu_split.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_unselect.png b/res/drawable-hdpi/ic_menu_unselect.png
new file mode 100644
index 0000000..178f314
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_unselect.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_tab_selected_contacts.png b/res/drawable-hdpi/ic_tab_selected_contacts.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_tab_selected_contacts.png
rename to res/drawable-hdpi/ic_tab_selected_contacts.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_tab_selected_dialer.png b/res/drawable-hdpi/ic_tab_selected_dialer.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_tab_selected_dialer.png
rename to res/drawable-hdpi/ic_tab_selected_dialer.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_tab_selected_friends_list.png b/res/drawable-hdpi/ic_tab_selected_friends_list.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_tab_selected_friends_list.png
rename to res/drawable-hdpi/ic_tab_selected_friends_list.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_tab_selected_recent.png b/res/drawable-hdpi/ic_tab_selected_recent.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_tab_selected_recent.png
rename to res/drawable-hdpi/ic_tab_selected_recent.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_tab_selected_starred.png b/res/drawable-hdpi/ic_tab_selected_starred.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_tab_selected_starred.png
rename to res/drawable-hdpi/ic_tab_selected_starred.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_tab_unselected_contacts.png b/res/drawable-hdpi/ic_tab_unselected_contacts.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_tab_unselected_contacts.png
rename to res/drawable-hdpi/ic_tab_unselected_contacts.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_tab_unselected_dialer.png b/res/drawable-hdpi/ic_tab_unselected_dialer.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_tab_unselected_dialer.png
rename to res/drawable-hdpi/ic_tab_unselected_dialer.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_tab_unselected_friends_list.png b/res/drawable-hdpi/ic_tab_unselected_friends_list.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_tab_unselected_friends_list.png
rename to res/drawable-hdpi/ic_tab_unselected_friends_list.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_tab_unselected_recent.png b/res/drawable-hdpi/ic_tab_unselected_recent.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_tab_unselected_recent.png
rename to res/drawable-hdpi/ic_tab_unselected_recent.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_tab_unselected_starred.png b/res/drawable-hdpi/ic_tab_unselected_starred.png
similarity index 100%
rename from res/drawable-hdpi-finger/ic_tab_unselected_starred.png
rename to res/drawable-hdpi/ic_tab_unselected_starred.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/infobar_dark.9.png b/res/drawable-hdpi/infobar_dark.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/infobar_dark.9.png
rename to res/drawable-hdpi/infobar_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_arrow_down.png b/res/drawable-hdpi/quickcontact_arrow_down.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_arrow_down.png
rename to res/drawable-hdpi/quickcontact_arrow_down.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_arrow_up.png b/res/drawable-hdpi/quickcontact_arrow_up.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_arrow_up.png
rename to res/drawable-hdpi/quickcontact_arrow_up.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_bottom_frame.9.png b/res/drawable-hdpi/quickcontact_bottom_frame.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_bottom_frame.9.png
rename to res/drawable-hdpi/quickcontact_bottom_frame.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_disambig_bottom_bg.9.png b/res/drawable-hdpi/quickcontact_disambig_bottom_bg.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_disambig_bottom_bg.9.png
rename to res/drawable-hdpi/quickcontact_disambig_bottom_bg.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_disambig_checkbox_off.png b/res/drawable-hdpi/quickcontact_disambig_checkbox_off.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_disambig_checkbox_off.png
rename to res/drawable-hdpi/quickcontact_disambig_checkbox_off.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_disambig_checkbox_on.png b/res/drawable-hdpi/quickcontact_disambig_checkbox_on.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_disambig_checkbox_on.png
rename to res/drawable-hdpi/quickcontact_disambig_checkbox_on.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_disambig_divider.9.png b/res/drawable-hdpi/quickcontact_disambig_divider.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_disambig_divider.9.png
rename to res/drawable-hdpi/quickcontact_disambig_divider.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_drop_shadow.9.png b/res/drawable-hdpi/quickcontact_drop_shadow.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_drop_shadow.9.png
rename to res/drawable-hdpi/quickcontact_drop_shadow.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_photo_frame.9.png b/res/drawable-hdpi/quickcontact_photo_frame.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_photo_frame.9.png
rename to res/drawable-hdpi/quickcontact_photo_frame.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_slider_background.png b/res/drawable-hdpi/quickcontact_slider_background.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_slider_background.png
rename to res/drawable-hdpi/quickcontact_slider_background.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_slider_btn_normal.9.png b/res/drawable-hdpi/quickcontact_slider_btn_normal.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_slider_btn_normal.9.png
rename to res/drawable-hdpi/quickcontact_slider_btn_normal.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_slider_btn_on.9.png b/res/drawable-hdpi/quickcontact_slider_btn_on.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_slider_btn_on.9.png
rename to res/drawable-hdpi/quickcontact_slider_btn_on.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_slider_btn_pressed.9.png b/res/drawable-hdpi/quickcontact_slider_btn_pressed.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_slider_btn_pressed.9.png
rename to res/drawable-hdpi/quickcontact_slider_btn_pressed.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_slider_btn_selected.9.png b/res/drawable-hdpi/quickcontact_slider_btn_selected.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_slider_btn_selected.9.png
rename to res/drawable-hdpi/quickcontact_slider_btn_selected.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_slider_grip_left.png b/res/drawable-hdpi/quickcontact_slider_grip_left.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_slider_grip_left.png
rename to res/drawable-hdpi/quickcontact_slider_grip_left.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_slider_grip_right.png b/res/drawable-hdpi/quickcontact_slider_grip_right.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_slider_grip_right.png
rename to res/drawable-hdpi/quickcontact_slider_grip_right.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_slider_presence_active.png b/res/drawable-hdpi/quickcontact_slider_presence_active.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_slider_presence_active.png
rename to res/drawable-hdpi/quickcontact_slider_presence_active.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_slider_presence_away.png b/res/drawable-hdpi/quickcontact_slider_presence_away.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_slider_presence_away.png
rename to res/drawable-hdpi/quickcontact_slider_presence_away.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_slider_presence_busy.png b/res/drawable-hdpi/quickcontact_slider_presence_busy.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_slider_presence_busy.png
rename to res/drawable-hdpi/quickcontact_slider_presence_busy.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_slider_presence_inactive.png b/res/drawable-hdpi/quickcontact_slider_presence_inactive.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_slider_presence_inactive.png
rename to res/drawable-hdpi/quickcontact_slider_presence_inactive.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/quickcontact_top_frame.9.png b/res/drawable-hdpi/quickcontact_top_frame.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/quickcontact_top_frame.9.png
rename to res/drawable-hdpi/quickcontact_top_frame.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/sym_action_add.png b/res/drawable-hdpi/sym_action_add.png
similarity index 100%
rename from res/drawable-hdpi-finger/sym_action_add.png
rename to res/drawable-hdpi/sym_action_add.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/sym_action_map.png b/res/drawable-hdpi/sym_action_map.png
similarity index 100%
rename from res/drawable-hdpi-finger/sym_action_map.png
rename to res/drawable-hdpi/sym_action_map.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/sym_action_organization.png b/res/drawable-hdpi/sym_action_organization.png
similarity index 100%
rename from res/drawable-hdpi-finger/sym_action_organization.png
rename to res/drawable-hdpi/sym_action_organization.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/sym_action_view_contact.png b/res/drawable-hdpi/sym_action_view_contact.png
similarity index 100%
rename from res/drawable-hdpi-finger/sym_action_view_contact.png
rename to res/drawable-hdpi/sym_action_view_contact.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/sym_note.png b/res/drawable-hdpi/sym_note.png
similarity index 100%
rename from res/drawable-hdpi-finger/sym_note.png
rename to res/drawable-hdpi/sym_note.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/tab_focused.9.png b/res/drawable-hdpi/tab_focused.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/tab_focused.9.png
rename to res/drawable-hdpi/tab_focused.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/tab_focused_bottom.9.png b/res/drawable-hdpi/tab_focused_bottom.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/tab_focused_bottom.9.png
rename to res/drawable-hdpi/tab_focused_bottom.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/tab_left_arrow.png b/res/drawable-hdpi/tab_left_arrow.png
similarity index 100%
rename from res/drawable-hdpi-finger/tab_left_arrow.png
rename to res/drawable-hdpi/tab_left_arrow.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/tab_pressed.9.png b/res/drawable-hdpi/tab_pressed.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/tab_pressed.9.png
rename to res/drawable-hdpi/tab_pressed.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/tab_pressed_bottom.9.png b/res/drawable-hdpi/tab_pressed_bottom.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/tab_pressed_bottom.9.png
rename to res/drawable-hdpi/tab_pressed_bottom.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/tab_right_arrow.png b/res/drawable-hdpi/tab_right_arrow.png
similarity index 100%
rename from res/drawable-hdpi-finger/tab_right_arrow.png
rename to res/drawable-hdpi/tab_right_arrow.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/tab_selected.9.png b/res/drawable-hdpi/tab_selected.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/tab_selected.9.png
rename to res/drawable-hdpi/tab_selected.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/tab_selected_bottom.9.png b/res/drawable-hdpi/tab_selected_bottom.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/tab_selected_bottom.9.png
rename to res/drawable-hdpi/tab_selected_bottom.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/tab_unselected.9.png b/res/drawable-hdpi/tab_unselected.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/tab_unselected.9.png
rename to res/drawable-hdpi/tab_unselected.9.png
Binary files differ
diff --git a/res/drawable-hdpi/title_bar_medium.9.png b/res/drawable-hdpi/title_bar_medium.9.png
new file mode 100644
index 0000000..311a54a
--- /dev/null
+++ b/res/drawable-hdpi/title_bar_medium.9.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/title_bar_shadow.9.png b/res/drawable-hdpi/title_bar_shadow.9.png
similarity index 100%
rename from res/drawable-hdpi-finger/title_bar_shadow.9.png
rename to res/drawable-hdpi/title_bar_shadow.9.png
Binary files differ
diff --git a/res/drawable-mdpi/aizy_bottom.png b/res/drawable-mdpi/aizy_bottom.png
new file mode 100644
index 0000000..1f3d332
--- /dev/null
+++ b/res/drawable-mdpi/aizy_bottom.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/drawable-mdpi-finger/badge_action_call.png b/res/drawable-mdpi/badge_action_call.png
similarity index 100%
rename from res/drawable-mdpi-finger/badge_action_call.png
rename to res/drawable-mdpi/badge_action_call.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/badge_action_sms.png b/res/drawable-mdpi/badge_action_sms.png
similarity index 100%
rename from res/drawable-mdpi-finger/badge_action_sms.png
rename to res/drawable-mdpi/badge_action_sms.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/bg_blk_search_contact.9.png b/res/drawable-mdpi/bg_blk_search_contact.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/bg_blk_search_contact.9.png
rename to res/drawable-mdpi/bg_blk_search_contact.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_circle_disable.png b/res/drawable-mdpi/btn_circle_disable.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_circle_disable.png
rename to res/drawable-mdpi/btn_circle_disable.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_circle_disable_focused.png b/res/drawable-mdpi/btn_circle_disable_focused.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_circle_disable_focused.png
rename to res/drawable-mdpi/btn_circle_disable_focused.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_circle_normal.png b/res/drawable-mdpi/btn_circle_normal.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_circle_normal.png
rename to res/drawable-mdpi/btn_circle_normal.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_circle_pressed.png b/res/drawable-mdpi/btn_circle_pressed.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_circle_pressed.png
rename to res/drawable-mdpi/btn_circle_pressed.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_circle_selected.png b/res/drawable-mdpi/btn_circle_selected.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_circle_selected.png
rename to res/drawable-mdpi/btn_circle_selected.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_left_disable.9.png b/res/drawable-mdpi/btn_dial_action_left_disable.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_left_disable.9.png
rename to res/drawable-mdpi/btn_dial_action_left_disable.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_left_disable_focused.9.png b/res/drawable-mdpi/btn_dial_action_left_disable_focused.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_left_disable_focused.9.png
rename to res/drawable-mdpi/btn_dial_action_left_disable_focused.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_left_normal.9.png b/res/drawable-mdpi/btn_dial_action_left_normal.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_left_normal.9.png
rename to res/drawable-mdpi/btn_dial_action_left_normal.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_left_pressed.9.png b/res/drawable-mdpi/btn_dial_action_left_pressed.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_left_pressed.9.png
rename to res/drawable-mdpi/btn_dial_action_left_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_left_selected.9.png b/res/drawable-mdpi/btn_dial_action_left_selected.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_left_selected.9.png
rename to res/drawable-mdpi/btn_dial_action_left_selected.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_middle_disable.9.png b/res/drawable-mdpi/btn_dial_action_middle_disable.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_middle_disable.9.png
rename to res/drawable-mdpi/btn_dial_action_middle_disable.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_middle_disable_focused.9.png b/res/drawable-mdpi/btn_dial_action_middle_disable_focused.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_middle_disable_focused.9.png
rename to res/drawable-mdpi/btn_dial_action_middle_disable_focused.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_middle_normal.9.png b/res/drawable-mdpi/btn_dial_action_middle_normal.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_middle_normal.9.png
rename to res/drawable-mdpi/btn_dial_action_middle_normal.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_middle_pressed.9.png b/res/drawable-mdpi/btn_dial_action_middle_pressed.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_middle_pressed.9.png
rename to res/drawable-mdpi/btn_dial_action_middle_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_middle_selected.9.png b/res/drawable-mdpi/btn_dial_action_middle_selected.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_middle_selected.9.png
rename to res/drawable-mdpi/btn_dial_action_middle_selected.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_right_disable.9.png b/res/drawable-mdpi/btn_dial_action_right_disable.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_right_disable.9.png
rename to res/drawable-mdpi/btn_dial_action_right_disable.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_right_disable_focused.9.png b/res/drawable-mdpi/btn_dial_action_right_disable_focused.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_right_disable_focused.9.png
rename to res/drawable-mdpi/btn_dial_action_right_disable_focused.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_right_normal.9.png b/res/drawable-mdpi/btn_dial_action_right_normal.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_right_normal.9.png
rename to res/drawable-mdpi/btn_dial_action_right_normal.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_right_pressed.9.png b/res/drawable-mdpi/btn_dial_action_right_pressed.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_right_pressed.9.png
rename to res/drawable-mdpi/btn_dial_action_right_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_action_right_selected.9.png b/res/drawable-mdpi/btn_dial_action_right_selected.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_action_right_selected.9.png
rename to res/drawable-mdpi/btn_dial_action_right_selected.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_normal.9.png b/res/drawable-mdpi/btn_dial_normal.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_normal.9.png
rename to res/drawable-mdpi/btn_dial_normal.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_pressed.9.png b/res/drawable-mdpi/btn_dial_pressed.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_pressed.9.png
rename to res/drawable-mdpi/btn_dial_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_selected.9.png b/res/drawable-mdpi/btn_dial_selected.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_selected.9.png
rename to res/drawable-mdpi/btn_dial_selected.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_textfield_activated.9.png b/res/drawable-mdpi/btn_dial_textfield_activated.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_textfield_activated.9.png
rename to res/drawable-mdpi/btn_dial_textfield_activated.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_textfield_normal.9.png b/res/drawable-mdpi/btn_dial_textfield_normal.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_textfield_normal.9.png
rename to res/drawable-mdpi/btn_dial_textfield_normal.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_textfield_pressed.9.png b/res/drawable-mdpi/btn_dial_textfield_pressed.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_textfield_pressed.9.png
rename to res/drawable-mdpi/btn_dial_textfield_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_dial_textfield_selected.9.png b/res/drawable-mdpi/btn_dial_textfield_selected.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_dial_textfield_selected.9.png
rename to res/drawable-mdpi/btn_dial_textfield_selected.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_search_dialog_default.9.png b/res/drawable-mdpi/btn_search_dialog_default.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_search_dialog_default.9.png
rename to res/drawable-mdpi/btn_search_dialog_default.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_search_dialog_pressed.9.png b/res/drawable-mdpi/btn_search_dialog_pressed.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_search_dialog_pressed.9.png
rename to res/drawable-mdpi/btn_search_dialog_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_search_dialog_selected.9.png b/res/drawable-mdpi/btn_search_dialog_selected.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/btn_search_dialog_selected.9.png
rename to res/drawable-mdpi/btn_search_dialog_selected.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/contact_picture_border_highlight.9.png b/res/drawable-mdpi/contact_picture_border_highlight.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/contact_picture_border_highlight.9.png
rename to res/drawable-mdpi/contact_picture_border_highlight.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/contact_picture_border_normal.9.png b/res/drawable-mdpi/contact_picture_border_normal.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/contact_picture_border_normal.9.png
rename to res/drawable-mdpi/contact_picture_border_normal.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/contact_picture_border_pressed.9.png b/res/drawable-mdpi/contact_picture_border_pressed.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/contact_picture_border_pressed.9.png
rename to res/drawable-mdpi/contact_picture_border_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_0_blk.png b/res/drawable-mdpi/dial_num_0_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_0_blk.png
rename to res/drawable-mdpi/dial_num_0_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_0_wht.png b/res/drawable-mdpi/dial_num_0_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_0_wht.png
rename to res/drawable-mdpi/dial_num_0_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_1_no_vm_blk.png b/res/drawable-mdpi/dial_num_1_no_vm_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_1_no_vm_blk.png
rename to res/drawable-mdpi/dial_num_1_no_vm_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_1_no_vm_wht.png b/res/drawable-mdpi/dial_num_1_no_vm_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_1_no_vm_wht.png
rename to res/drawable-mdpi/dial_num_1_no_vm_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_2_blk.png b/res/drawable-mdpi/dial_num_2_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_2_blk.png
rename to res/drawable-mdpi/dial_num_2_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_2_wht.png b/res/drawable-mdpi/dial_num_2_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_2_wht.png
rename to res/drawable-mdpi/dial_num_2_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_3_blk.png b/res/drawable-mdpi/dial_num_3_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_3_blk.png
rename to res/drawable-mdpi/dial_num_3_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_3_wht.png b/res/drawable-mdpi/dial_num_3_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_3_wht.png
rename to res/drawable-mdpi/dial_num_3_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_4_blk.png b/res/drawable-mdpi/dial_num_4_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_4_blk.png
rename to res/drawable-mdpi/dial_num_4_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_4_wht.png b/res/drawable-mdpi/dial_num_4_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_4_wht.png
rename to res/drawable-mdpi/dial_num_4_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_5_blk.png b/res/drawable-mdpi/dial_num_5_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_5_blk.png
rename to res/drawable-mdpi/dial_num_5_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_5_wht.png b/res/drawable-mdpi/dial_num_5_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_5_wht.png
rename to res/drawable-mdpi/dial_num_5_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_6_blk.png b/res/drawable-mdpi/dial_num_6_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_6_blk.png
rename to res/drawable-mdpi/dial_num_6_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_6_wht.png b/res/drawable-mdpi/dial_num_6_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_6_wht.png
rename to res/drawable-mdpi/dial_num_6_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_7_blk.png b/res/drawable-mdpi/dial_num_7_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_7_blk.png
rename to res/drawable-mdpi/dial_num_7_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_7_wht.png b/res/drawable-mdpi/dial_num_7_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_7_wht.png
rename to res/drawable-mdpi/dial_num_7_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_8_blk.png b/res/drawable-mdpi/dial_num_8_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_8_blk.png
rename to res/drawable-mdpi/dial_num_8_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_8_wht.png b/res/drawable-mdpi/dial_num_8_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_8_wht.png
rename to res/drawable-mdpi/dial_num_8_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_9_blk.png b/res/drawable-mdpi/dial_num_9_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_9_blk.png
rename to res/drawable-mdpi/dial_num_9_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_9_wht.png b/res/drawable-mdpi/dial_num_9_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_9_wht.png
rename to res/drawable-mdpi/dial_num_9_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_pound_blk.png b/res/drawable-mdpi/dial_num_pound_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_pound_blk.png
rename to res/drawable-mdpi/dial_num_pound_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_pound_wht.png b/res/drawable-mdpi/dial_num_pound_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_pound_wht.png
rename to res/drawable-mdpi/dial_num_pound_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_star_blk.png b/res/drawable-mdpi/dial_num_star_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_star_blk.png
rename to res/drawable-mdpi/dial_num_star_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/dial_num_star_wht.png b/res/drawable-mdpi/dial_num_star_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/dial_num_star_wht.png
rename to res/drawable-mdpi/dial_num_star_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/divider_vertical_dark.9.png b/res/drawable-mdpi/divider_vertical_dark.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/divider_vertical_dark.9.png
rename to res/drawable-mdpi/divider_vertical_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/edit_focused.png b/res/drawable-mdpi/edit_focused.png
new file mode 100644
index 0000000..cf65d88
--- /dev/null
+++ b/res/drawable-mdpi/edit_focused.png
Binary files differ
diff --git a/res/drawable-mdpi/edit_normal.png b/res/drawable-mdpi/edit_normal.png
new file mode 100644
index 0000000..4e66379
--- /dev/null
+++ b/res/drawable-mdpi/edit_normal.png
Binary files differ
diff --git a/res/drawable-mdpi/edit_pressed.png b/res/drawable-mdpi/edit_pressed.png
new file mode 100644
index 0000000..537795d
--- /dev/null
+++ b/res/drawable-mdpi/edit_pressed.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_aggregated.png b/res/drawable-mdpi/ic_aggregated.png
new file mode 100644
index 0000000..7c2e2b0
--- /dev/null
+++ b/res/drawable-mdpi/ic_aggregated.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_btn_round_less.png b/res/drawable-mdpi/ic_btn_round_less.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_btn_round_less.png
rename to res/drawable-mdpi/ic_btn_round_less.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_btn_round_minus.png b/res/drawable-mdpi/ic_btn_round_minus.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_btn_round_minus.png
rename to res/drawable-mdpi/ic_btn_round_minus.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_btn_round_more.png b/res/drawable-mdpi/ic_btn_round_more.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_btn_round_more.png
rename to res/drawable-mdpi/ic_btn_round_more.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_btn_round_plus.png b/res/drawable-mdpi/ic_btn_round_plus.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_btn_round_plus.png
rename to res/drawable-mdpi/ic_btn_round_plus.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_call_log_header_incoming_call.png b/res/drawable-mdpi/ic_call_log_header_incoming_call.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_call_log_header_incoming_call.png
rename to res/drawable-mdpi/ic_call_log_header_incoming_call.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_call_log_header_missed_call.png b/res/drawable-mdpi/ic_call_log_header_missed_call.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_call_log_header_missed_call.png
rename to res/drawable-mdpi/ic_call_log_header_missed_call.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_call_log_header_outgoing_call.png b/res/drawable-mdpi/ic_call_log_header_outgoing_call.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_call_log_header_outgoing_call.png
rename to res/drawable-mdpi/ic_call_log_header_outgoing_call.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_call_log_list_incoming_call.png b/res/drawable-mdpi/ic_call_log_list_incoming_call.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_call_log_list_incoming_call.png
rename to res/drawable-mdpi/ic_call_log_list_incoming_call.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_call_log_list_missed_call.png b/res/drawable-mdpi/ic_call_log_list_missed_call.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_call_log_list_missed_call.png
rename to res/drawable-mdpi/ic_call_log_list_missed_call.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_call_log_list_outgoing_call.png b/res/drawable-mdpi/ic_call_log_list_outgoing_call.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_call_log_list_outgoing_call.png
rename to res/drawable-mdpi/ic_call_log_list_outgoing_call.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_contact_list_picture.png b/res/drawable-mdpi/ic_contact_list_picture.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_contact_list_picture.png
rename to res/drawable-mdpi/ic_contact_list_picture.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_contact_picture.png b/res/drawable-mdpi/ic_contact_picture.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_contact_picture.png
rename to res/drawable-mdpi/ic_contact_picture.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_contact_picture_2.png b/res/drawable-mdpi/ic_contact_picture_2.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_contact_picture_2.png
rename to res/drawable-mdpi/ic_contact_picture_2.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_contact_picture_3.png b/res/drawable-mdpi/ic_contact_picture_3.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_contact_picture_3.png
rename to res/drawable-mdpi/ic_contact_picture_3.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_default_number.png b/res/drawable-mdpi/ic_default_number.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_default_number.png
rename to res/drawable-mdpi/ic_default_number.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_dial_action_call.png b/res/drawable-mdpi/ic_dial_action_call.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_dial_action_call.png
rename to res/drawable-mdpi/ic_dial_action_call.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_dial_action_delete.png b/res/drawable-mdpi/ic_dial_action_delete.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_dial_action_delete.png
rename to res/drawable-mdpi/ic_dial_action_delete.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_dial_action_voice_mail.png b/res/drawable-mdpi/ic_dial_action_voice_mail.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_dial_action_voice_mail.png
rename to res/drawable-mdpi/ic_dial_action_voice_mail.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_dial_number_blk.png b/res/drawable-mdpi/ic_dial_number_blk.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_dial_number_blk.png
rename to res/drawable-mdpi/ic_dial_number_blk.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_dial_number_wht.png b/res/drawable-mdpi/ic_dial_number_wht.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_dial_number_wht.png
rename to res/drawable-mdpi/ic_dial_number_wht.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_dialer_fork_add_call.png b/res/drawable-mdpi/ic_dialer_fork_add_call.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_dialer_fork_add_call.png
rename to res/drawable-mdpi/ic_dialer_fork_add_call.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_dialer_fork_current_call.png b/res/drawable-mdpi/ic_dialer_fork_current_call.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_dialer_fork_current_call.png
rename to res/drawable-mdpi/ic_dialer_fork_current_call.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_dialer_fork_tt_keypad.png b/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_dialer_fork_tt_keypad.png
rename to res/drawable-mdpi/ic_dialer_fork_tt_keypad.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_join.png b/res/drawable-mdpi/ic_join.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_join.png
rename to res/drawable-mdpi/ic_join.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_account_list.png b/res/drawable-mdpi/ic_menu_account_list.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_menu_account_list.png
rename to res/drawable-mdpi/ic_menu_account_list.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_add_picture.png b/res/drawable-mdpi/ic_menu_add_picture.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_menu_add_picture.png
rename to res/drawable-mdpi/ic_menu_add_picture.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_display_all.png b/res/drawable-mdpi/ic_menu_display_all.png
new file mode 100644
index 0000000..61a9e35
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_display_all.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_display_selected.png b/res/drawable-mdpi/ic_menu_display_selected.png
new file mode 100644
index 0000000..b4ec7a8
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_display_selected.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_import_export.png b/res/drawable-mdpi/ic_menu_import_export.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_menu_import_export.png
rename to res/drawable-mdpi/ic_menu_import_export.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_merge.png b/res/drawable-mdpi/ic_menu_merge.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_menu_merge.png
rename to res/drawable-mdpi/ic_menu_merge.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_select.png b/res/drawable-mdpi/ic_menu_select.png
new file mode 100644
index 0000000..29e3d7e
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_select.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_split.png b/res/drawable-mdpi/ic_menu_split.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_menu_split.png
rename to res/drawable-mdpi/ic_menu_split.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_unselect.png b/res/drawable-mdpi/ic_menu_unselect.png
new file mode 100644
index 0000000..2b69bc8
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_unselect.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_tab_selected_contacts.png b/res/drawable-mdpi/ic_tab_selected_contacts.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_tab_selected_contacts.png
rename to res/drawable-mdpi/ic_tab_selected_contacts.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_tab_selected_dialer.png b/res/drawable-mdpi/ic_tab_selected_dialer.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_tab_selected_dialer.png
rename to res/drawable-mdpi/ic_tab_selected_dialer.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_tab_selected_friends_list.png b/res/drawable-mdpi/ic_tab_selected_friends_list.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_tab_selected_friends_list.png
rename to res/drawable-mdpi/ic_tab_selected_friends_list.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_tab_selected_recent.png b/res/drawable-mdpi/ic_tab_selected_recent.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_tab_selected_recent.png
rename to res/drawable-mdpi/ic_tab_selected_recent.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_tab_selected_starred.png b/res/drawable-mdpi/ic_tab_selected_starred.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_tab_selected_starred.png
rename to res/drawable-mdpi/ic_tab_selected_starred.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_tab_unselected_contacts.png b/res/drawable-mdpi/ic_tab_unselected_contacts.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_tab_unselected_contacts.png
rename to res/drawable-mdpi/ic_tab_unselected_contacts.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_tab_unselected_dialer.png b/res/drawable-mdpi/ic_tab_unselected_dialer.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_tab_unselected_dialer.png
rename to res/drawable-mdpi/ic_tab_unselected_dialer.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_tab_unselected_friends_list.png b/res/drawable-mdpi/ic_tab_unselected_friends_list.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_tab_unselected_friends_list.png
rename to res/drawable-mdpi/ic_tab_unselected_friends_list.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_tab_unselected_recent.png b/res/drawable-mdpi/ic_tab_unselected_recent.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_tab_unselected_recent.png
rename to res/drawable-mdpi/ic_tab_unselected_recent.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_tab_unselected_starred.png b/res/drawable-mdpi/ic_tab_unselected_starred.png
similarity index 100%
rename from res/drawable-mdpi-finger/ic_tab_unselected_starred.png
rename to res/drawable-mdpi/ic_tab_unselected_starred.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/infobar_dark.9.png b/res/drawable-mdpi/infobar_dark.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/infobar_dark.9.png
rename to res/drawable-mdpi/infobar_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_arrow_down.png b/res/drawable-mdpi/quickcontact_arrow_down.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_arrow_down.png
rename to res/drawable-mdpi/quickcontact_arrow_down.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_arrow_up.png b/res/drawable-mdpi/quickcontact_arrow_up.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_arrow_up.png
rename to res/drawable-mdpi/quickcontact_arrow_up.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_bottom_frame.9.png b/res/drawable-mdpi/quickcontact_bottom_frame.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_bottom_frame.9.png
rename to res/drawable-mdpi/quickcontact_bottom_frame.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_disambig_bottom_bg.9.png b/res/drawable-mdpi/quickcontact_disambig_bottom_bg.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_disambig_bottom_bg.9.png
rename to res/drawable-mdpi/quickcontact_disambig_bottom_bg.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_disambig_checkbox_off.png b/res/drawable-mdpi/quickcontact_disambig_checkbox_off.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_disambig_checkbox_off.png
rename to res/drawable-mdpi/quickcontact_disambig_checkbox_off.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_disambig_checkbox_on.png b/res/drawable-mdpi/quickcontact_disambig_checkbox_on.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_disambig_checkbox_on.png
rename to res/drawable-mdpi/quickcontact_disambig_checkbox_on.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_disambig_divider.9.png b/res/drawable-mdpi/quickcontact_disambig_divider.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_disambig_divider.9.png
rename to res/drawable-mdpi/quickcontact_disambig_divider.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_drop_shadow.9.png b/res/drawable-mdpi/quickcontact_drop_shadow.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_drop_shadow.9.png
rename to res/drawable-mdpi/quickcontact_drop_shadow.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_photo_frame.9.png b/res/drawable-mdpi/quickcontact_photo_frame.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_photo_frame.9.png
rename to res/drawable-mdpi/quickcontact_photo_frame.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_slider_background.png b/res/drawable-mdpi/quickcontact_slider_background.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_slider_background.png
rename to res/drawable-mdpi/quickcontact_slider_background.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_slider_btn_normal.9.png b/res/drawable-mdpi/quickcontact_slider_btn_normal.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_slider_btn_normal.9.png
rename to res/drawable-mdpi/quickcontact_slider_btn_normal.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_slider_btn_on.9.png b/res/drawable-mdpi/quickcontact_slider_btn_on.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_slider_btn_on.9.png
rename to res/drawable-mdpi/quickcontact_slider_btn_on.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_slider_btn_pressed.9.png b/res/drawable-mdpi/quickcontact_slider_btn_pressed.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_slider_btn_pressed.9.png
rename to res/drawable-mdpi/quickcontact_slider_btn_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_slider_btn_selected.9.png b/res/drawable-mdpi/quickcontact_slider_btn_selected.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_slider_btn_selected.9.png
rename to res/drawable-mdpi/quickcontact_slider_btn_selected.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_slider_grip_left.png b/res/drawable-mdpi/quickcontact_slider_grip_left.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_slider_grip_left.png
rename to res/drawable-mdpi/quickcontact_slider_grip_left.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_slider_grip_right.png b/res/drawable-mdpi/quickcontact_slider_grip_right.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_slider_grip_right.png
rename to res/drawable-mdpi/quickcontact_slider_grip_right.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_slider_presence_active.png b/res/drawable-mdpi/quickcontact_slider_presence_active.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_slider_presence_active.png
rename to res/drawable-mdpi/quickcontact_slider_presence_active.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_slider_presence_away.png b/res/drawable-mdpi/quickcontact_slider_presence_away.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_slider_presence_away.png
rename to res/drawable-mdpi/quickcontact_slider_presence_away.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_slider_presence_busy.png b/res/drawable-mdpi/quickcontact_slider_presence_busy.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_slider_presence_busy.png
rename to res/drawable-mdpi/quickcontact_slider_presence_busy.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_slider_presence_inactive.png b/res/drawable-mdpi/quickcontact_slider_presence_inactive.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_slider_presence_inactive.png
rename to res/drawable-mdpi/quickcontact_slider_presence_inactive.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/quickcontact_top_frame.9.png b/res/drawable-mdpi/quickcontact_top_frame.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/quickcontact_top_frame.9.png
rename to res/drawable-mdpi/quickcontact_top_frame.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/sym_action_add.png b/res/drawable-mdpi/sym_action_add.png
similarity index 100%
rename from res/drawable-mdpi-finger/sym_action_add.png
rename to res/drawable-mdpi/sym_action_add.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/sym_action_map.png b/res/drawable-mdpi/sym_action_map.png
similarity index 100%
rename from res/drawable-mdpi-finger/sym_action_map.png
rename to res/drawable-mdpi/sym_action_map.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/sym_action_organization.png b/res/drawable-mdpi/sym_action_organization.png
similarity index 100%
rename from res/drawable-mdpi-finger/sym_action_organization.png
rename to res/drawable-mdpi/sym_action_organization.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/sym_action_view_contact.png b/res/drawable-mdpi/sym_action_view_contact.png
similarity index 100%
rename from res/drawable-mdpi-finger/sym_action_view_contact.png
rename to res/drawable-mdpi/sym_action_view_contact.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/sym_note.png b/res/drawable-mdpi/sym_note.png
similarity index 100%
rename from res/drawable-mdpi-finger/sym_note.png
rename to res/drawable-mdpi/sym_note.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/tab_focused.9.png b/res/drawable-mdpi/tab_focused.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/tab_focused.9.png
rename to res/drawable-mdpi/tab_focused.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/tab_focused_bottom.9.png b/res/drawable-mdpi/tab_focused_bottom.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/tab_focused_bottom.9.png
rename to res/drawable-mdpi/tab_focused_bottom.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/tab_left_arrow.png b/res/drawable-mdpi/tab_left_arrow.png
similarity index 100%
rename from res/drawable-mdpi-finger/tab_left_arrow.png
rename to res/drawable-mdpi/tab_left_arrow.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/tab_pressed.9.png b/res/drawable-mdpi/tab_pressed.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/tab_pressed.9.png
rename to res/drawable-mdpi/tab_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/tab_pressed_bottom.9.png b/res/drawable-mdpi/tab_pressed_bottom.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/tab_pressed_bottom.9.png
rename to res/drawable-mdpi/tab_pressed_bottom.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/tab_right_arrow.png b/res/drawable-mdpi/tab_right_arrow.png
similarity index 100%
rename from res/drawable-mdpi-finger/tab_right_arrow.png
rename to res/drawable-mdpi/tab_right_arrow.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/tab_selected.9.png b/res/drawable-mdpi/tab_selected.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/tab_selected.9.png
rename to res/drawable-mdpi/tab_selected.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/tab_selected_bottom.9.png b/res/drawable-mdpi/tab_selected_bottom.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/tab_selected_bottom.9.png
rename to res/drawable-mdpi/tab_selected_bottom.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/tab_unselected.9.png b/res/drawable-mdpi/tab_unselected.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/tab_unselected.9.png
rename to res/drawable-mdpi/tab_unselected.9.png
Binary files differ
diff --git a/res/drawable-mdpi/title_bar_medium.9.png b/res/drawable-mdpi/title_bar_medium.9.png
new file mode 100644
index 0000000..2d41d02
--- /dev/null
+++ b/res/drawable-mdpi/title_bar_medium.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/title_bar_shadow.9.png b/res/drawable-mdpi/title_bar_shadow.9.png
similarity index 100%
rename from res/drawable-mdpi-finger/title_bar_shadow.9.png
rename to res/drawable-mdpi/title_bar_shadow.9.png
Binary files differ
diff --git a/res/drawable-finger/btn_circle.xml b/res/drawable/btn_circle.xml
similarity index 100%
rename from res/drawable-finger/btn_circle.xml
rename to res/drawable/btn_circle.xml
diff --git a/res/drawable-finger/btn_contact_picture.xml b/res/drawable/btn_contact_picture.xml
similarity index 100%
rename from res/drawable-finger/btn_contact_picture.xml
rename to res/drawable/btn_contact_picture.xml
diff --git a/res/drawable-finger/btn_dial.xml b/res/drawable/btn_dial.xml
similarity index 100%
rename from res/drawable-finger/btn_dial.xml
rename to res/drawable/btn_dial.xml
diff --git a/res/drawable-finger/btn_dial_action.xml b/res/drawable/btn_dial_action.xml
similarity index 100%
rename from res/drawable-finger/btn_dial_action.xml
rename to res/drawable/btn_dial_action.xml
diff --git a/res/drawable-finger/btn_dial_delete.xml b/res/drawable/btn_dial_delete.xml
similarity index 100%
rename from res/drawable-finger/btn_dial_delete.xml
rename to res/drawable/btn_dial_delete.xml
diff --git a/res/drawable-finger/btn_dial_voicemail.xml b/res/drawable/btn_dial_voicemail.xml
similarity index 100%
rename from res/drawable-finger/btn_dial_voicemail.xml
rename to res/drawable/btn_dial_voicemail.xml
diff --git a/res/drawable-finger/dial_num_0.xml b/res/drawable/dial_num_0.xml
similarity index 100%
rename from res/drawable-finger/dial_num_0.xml
rename to res/drawable/dial_num_0.xml
diff --git a/res/drawable-finger/dial_num_1_no_vm.xml b/res/drawable/dial_num_1_no_vm.xml
similarity index 100%
rename from res/drawable-finger/dial_num_1_no_vm.xml
rename to res/drawable/dial_num_1_no_vm.xml
diff --git a/res/drawable-finger/dial_num_2.xml b/res/drawable/dial_num_2.xml
similarity index 100%
rename from res/drawable-finger/dial_num_2.xml
rename to res/drawable/dial_num_2.xml
diff --git a/res/drawable-finger/dial_num_3.xml b/res/drawable/dial_num_3.xml
similarity index 100%
rename from res/drawable-finger/dial_num_3.xml
rename to res/drawable/dial_num_3.xml
diff --git a/res/drawable-finger/dial_num_4.xml b/res/drawable/dial_num_4.xml
similarity index 100%
rename from res/drawable-finger/dial_num_4.xml
rename to res/drawable/dial_num_4.xml
diff --git a/res/drawable-finger/dial_num_5.xml b/res/drawable/dial_num_5.xml
similarity index 100%
rename from res/drawable-finger/dial_num_5.xml
rename to res/drawable/dial_num_5.xml
diff --git a/res/drawable-finger/dial_num_6.xml b/res/drawable/dial_num_6.xml
similarity index 100%
rename from res/drawable-finger/dial_num_6.xml
rename to res/drawable/dial_num_6.xml
diff --git a/res/drawable-finger/dial_num_7.xml b/res/drawable/dial_num_7.xml
similarity index 100%
rename from res/drawable-finger/dial_num_7.xml
rename to res/drawable/dial_num_7.xml
diff --git a/res/drawable-finger/dial_num_8.xml b/res/drawable/dial_num_8.xml
similarity index 100%
rename from res/drawable-finger/dial_num_8.xml
rename to res/drawable/dial_num_8.xml
diff --git a/res/drawable-finger/dial_num_9.xml b/res/drawable/dial_num_9.xml
similarity index 100%
rename from res/drawable-finger/dial_num_9.xml
rename to res/drawable/dial_num_9.xml
diff --git a/res/drawable-finger/dial_num_pound.xml b/res/drawable/dial_num_pound.xml
similarity index 100%
rename from res/drawable-finger/dial_num_pound.xml
rename to res/drawable/dial_num_pound.xml
diff --git a/res/drawable-finger/dial_num_star.xml b/res/drawable/dial_num_star.xml
similarity index 100%
rename from res/drawable-finger/dial_num_star.xml
rename to res/drawable/dial_num_star.xml
diff --git a/res/drawable/directory_bg.9.png b/res/drawable/directory_bg.9.png
new file mode 100644
index 0000000..80578cd
--- /dev/null
+++ b/res/drawable/directory_bg.9.png
Binary files differ
diff --git a/res/drawable/edit.xml b/res/drawable/edit.xml
new file mode 100644
index 0000000..12bd067
--- /dev/null
+++ b/res/drawable/edit.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true"
+ android:drawable="@drawable/edit_pressed" />
+ <item android:state_focused="true"
+ android:drawable="@drawable/edit_focused" />
+ <item android:drawable="@drawable/edit_normal" />
+</selector>
\ No newline at end of file
diff --git a/res/drawable-finger/ic_tab_contacts.xml b/res/drawable/ic_tab_contacts.xml
similarity index 100%
rename from res/drawable-finger/ic_tab_contacts.xml
rename to res/drawable/ic_tab_contacts.xml
diff --git a/res/drawable-finger/ic_tab_dialer.xml b/res/drawable/ic_tab_dialer.xml
similarity index 100%
rename from res/drawable-finger/ic_tab_dialer.xml
rename to res/drawable/ic_tab_dialer.xml
diff --git a/res/drawable-finger/ic_tab_recent.xml b/res/drawable/ic_tab_recent.xml
similarity index 100%
rename from res/drawable-finger/ic_tab_recent.xml
rename to res/drawable/ic_tab_recent.xml
diff --git a/res/drawable-finger/ic_tab_starred.xml b/res/drawable/ic_tab_starred.xml
similarity index 100%
rename from res/drawable-finger/ic_tab_starred.xml
rename to res/drawable/ic_tab_starred.xml
diff --git a/res/drawable-finger/quickcontact_disambig_checkbox.xml b/res/drawable/quickcontact_disambig_checkbox.xml
similarity index 100%
rename from res/drawable-finger/quickcontact_disambig_checkbox.xml
rename to res/drawable/quickcontact_disambig_checkbox.xml
diff --git a/res/drawable-finger/quickcontact_slider_btn.xml b/res/drawable/quickcontact_slider_btn.xml
similarity index 100%
rename from res/drawable-finger/quickcontact_slider_btn.xml
rename to res/drawable/quickcontact_slider_btn.xml
diff --git a/res/drawable-finger/tab_bottom.xml b/res/drawable/tab_bottom.xml
similarity index 100%
rename from res/drawable-finger/tab_bottom.xml
rename to res/drawable/tab_bottom.xml
diff --git a/res/drawable-finger/tab_indicator_bg.xml b/res/drawable/tab_indicator_bg.xml
similarity index 100%
rename from res/drawable-finger/tab_indicator_bg.xml
rename to res/drawable/tab_indicator_bg.xml
diff --git a/res/layout-land-finger/twelve_key_dialer.xml b/res/layout-land/twelve_key_dialer.xml
similarity index 100%
rename from res/layout-land-finger/twelve_key_dialer.xml
rename to res/layout-land/twelve_key_dialer.xml
diff --git a/res/layout-long-land-finger/twelve_key_dialer.xml b/res/layout-long-land/twelve_key_dialer.xml
similarity index 100%
rename from res/layout-long-land-finger/twelve_key_dialer.xml
rename to res/layout-long-land/twelve_key_dialer.xml
diff --git a/res/layout-long-finger/dialpad.xml b/res/layout-long/dialpad.xml
similarity index 100%
rename from res/layout-long-finger/dialpad.xml
rename to res/layout-long/dialpad.xml
diff --git a/res/layout-long-finger/twelve_key_dialer.xml b/res/layout-long/twelve_key_dialer.xml
similarity index 100%
rename from res/layout-long-finger/twelve_key_dialer.xml
rename to res/layout-long/twelve_key_dialer.xml
diff --git a/res/layout-long-finger/voicemail_dial_delete.xml b/res/layout-long/voicemail_dial_delete.xml
similarity index 100%
rename from res/layout-long-finger/voicemail_dial_delete.xml
rename to res/layout-long/voicemail_dial_delete.xml
diff --git a/res/layout/aizy_popup_window.xml b/res/layout/aizy_popup_window.xml
new file mode 100644
index 0000000..470f116
--- /dev/null
+++ b/res/layout/aizy_popup_window.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/aizy_bottom"
+ >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/caption"
+ android:width="75dip"
+ android:textSize="50sp"
+ android:paddingLeft="20dip"
+ android:gravity="center" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-finger/call_detail.xml b/res/layout/call_detail.xml
similarity index 100%
rename from res/layout-finger/call_detail.xml
rename to res/layout/call_detail.xml
diff --git a/res/layout-finger/call_detail_list_item.xml b/res/layout/call_detail_list_item.xml
similarity index 100%
rename from res/layout-finger/call_detail_list_item.xml
rename to res/layout/call_detail_list_item.xml
diff --git a/res/layout-finger/contact_card_layout.xml b/res/layout/contact_card_layout.xml
similarity index 100%
rename from res/layout-finger/contact_card_layout.xml
rename to res/layout/contact_card_layout.xml
diff --git a/res/layout/contact_detail_activity.xml b/res/layout/contact_detail_activity.xml
new file mode 100644
index 0000000..9b63fec
--- /dev/null
+++ b/res/layout/contact_detail_activity.xml
@@ -0,0 +1,27 @@
+<?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:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <fragment android:name="com.android.contacts.views.detail.ContactDetailFragment"
+ android:id="@+id/contact_detail_fragment"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+</LinearLayout>
diff --git a/res/layout/contact_detail_fragment.xml b/res/layout/contact_detail_fragment.xml
new file mode 100644
index 0000000..8c1bef4
--- /dev/null
+++ b/res/layout/contact_detail_fragment.xml
@@ -0,0 +1,54 @@
+<?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/contact_detail"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.contacts.views.detail.ContactDetailHeaderView
+ 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="0px"
+ android:layout_weight="1"
+ android:background="@drawable/title_bar_shadow"
+ />
+
+ <ScrollView android:id="@android:id/empty"
+ android:layout_width="match_parent"
+ android:layout_height="0px"
+ android:layout_weight="1"
+ android:visibility="gone"
+ >
+ <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>
+</LinearLayout>
+
diff --git a/res/layout/contact_detail_header_view.xml b/res/layout/contact_detail_header_view.xml
new file mode 100644
index 0000000..3e47d51
--- /dev/null
+++ b/res/layout/contact_detail_header_view.xml
@@ -0,0 +1,109 @@
+<?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/banner"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:background="@drawable/title_bar_medium"
+ android:paddingRight="5dip">
+
+ <android.widget.QuickContactBadge android:id="@+id/photo"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="8dip"
+ android:layout_marginLeft="-1dip"
+ style="@*android:style/Widget.QuickContactBadge.WindowSmall" />
+ />
+
+ <LinearLayout
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:layout_gravity="center_vertical" >
+
+ <TextView android:id="@+id/name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textStyle="bold"
+ android:shadowColor="#BB000000"
+ android:shadowRadius="2.75"
+ />
+
+ <TextView android:id="@+id/phonetic_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:layout_marginTop="-2dip"
+ android:visibility="gone"
+ />
+
+ <TextView android:id="@+id/status"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:layout_marginTop="-2dip"
+ android:visibility="gone"
+ />
+
+ <TextView android:id="@+id/status_date"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textSize="12sp"
+ android:layout_marginTop="-2dip"
+ android:visibility="gone"
+ />
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/presence"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingLeft="3dip"
+ android:paddingRight="6dip"
+ android:visibility="gone"
+ />
+
+ <ImageButton
+ android:id="@+id/edit"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:visibility="visible"
+ android:contentDescription="@string/description_edit"
+ android:src="@drawable/edit"
+ android:paddingRight="15dip"
+ android:background="#0000" />
+
+ <CheckBox
+ android:id="@+id/star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="@string/description_star"
+ style="?android:attr/starStyle" />
+</LinearLayout>
diff --git a/res/layout/contact_editor_activity.xml b/res/layout/contact_editor_activity.xml
new file mode 100644
index 0000000..3768f6a
--- /dev/null
+++ b/res/layout/contact_editor_activity.xml
@@ -0,0 +1,27 @@
+<?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:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <fragment android:name="com.android.contacts.views.editor.ContactEditorFragment"
+ android:id="@+id/contact_editor_fragment"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+</LinearLayout>
diff --git a/res/layout/contact_editor_fragment.xml b/res/layout/contact_editor_fragment.xml
new file mode 100644
index 0000000..32ac464
--- /dev/null
+++ b/res/layout/contact_editor_fragment.xml
@@ -0,0 +1,40 @@
+<?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/contact_detail"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.contacts.views.editor.ContactEditorHeaderView
+ android:id="@+id/contact_header_widget"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="0px"
+ android:layout_weight="1">
+ <LinearLayout android:id="@+id/field_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:background="@drawable/title_bar_shadow"
+ />
+ </ScrollView>
+</LinearLayout>
+
diff --git a/res/layout/contact_editor_header_view.xml b/res/layout/contact_editor_header_view.xml
new file mode 100644
index 0000000..fb5596e
--- /dev/null
+++ b/res/layout/contact_editor_header_view.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/banner"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:background="@drawable/title_bar_medium"
+ android:paddingRight="5dip">
+
+ <TextView android:id="@+id/caption"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textStyle="bold"
+ android:shadowColor="#BB000000"
+ android:shadowRadius="2.75"
+ android:text="@string/edit_contact"
+ android:layout_gravity="center_vertical"
+ />
+
+ <LinearLayout
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:layout_gravity="center_vertical" />
+
+ <TextView android:id="@+id/merge_info"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textStyle="bold"
+ android:shadowColor="#BB000000"
+ android:shadowRadius="2.75"
+ android:layout_gravity="center_vertical"
+ />
+</LinearLayout>
diff --git a/res/layout/contact_field_editor_email_fragment.xml b/res/layout/contact_field_editor_email_fragment.xml
new file mode 100644
index 0000000..96fbbd2
--- /dev/null
+++ b/res/layout/contact_field_editor_email_fragment.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="300dip"
+ android:layout_height="240dip"
+ android:orientation="vertical"
+>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+ <ImageButton
+ android:id="@+id/email_edit_delete"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@style/MinusButton" />
+ <HorizontalScrollView
+ android:id="@+id/email_types"
+ android:layout_width="160dip"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="60dip">
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <Button
+ android:id="@+id/email_type_home"
+ android:layout_width="80dip"
+ android:layout_height="wrap_content" />
+ <Button
+ android:id="@+id/email_type_work"
+ android:layout_width="80dip"
+ android:layout_height="wrap_content" />
+ <Button
+ android:id="@+id/email_type_other"
+ android:layout_width="80dip"
+ android:layout_height="wrap_content" />
+ <Button
+ android:id="@+id/email_type_custom"
+ android:layout_width="80dip"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+ </HorizontalScrollView>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/email_edit_custom_type"
+ android:layout_width="150dip"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_alignParentRight="true">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/edit_email_type_caption"
+ />
+ <EditText
+ android:id="@+id/email_edit_type_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ />
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="5dip"
+ android:text="@string/edit_email_caption"
+ />
+ <EditText
+ android:id="@+id/email_edit_text"
+ android:layout_width="290dip"
+ android:layout_height="wrap_content"
+ android:inputType="textEmailAddress"
+ android:imeOptions="actionDone"
+ />
+
+ <LinearLayout
+ android:layout_width="290dip"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="5dip"
+ android:orientation="horizontal"
+ >
+ <Button android:id="@+id/btn_ok"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:text="@android:string/ok"
+ android:layout_weight="1"
+ />
+ <Button android:id="@+id/btn_cancel"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:text="@android:string/cancel"
+ android:layout_weight="1"
+ />
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/layout-finger/contact_options.xml b/res/layout/contact_options.xml
similarity index 100%
rename from res/layout-finger/contact_options.xml
rename to res/layout/contact_options.xml
diff --git a/res/layout/contacts_list_content.xml b/res/layout/contacts_list_content.xml
new file mode 100644
index 0000000..f5bb785
--- /dev/null
+++ b/res/layout/contacts_list_content.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pinned_header_list_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ >
+
+ <com.android.contacts.list.ContactListAizyView
+ android:id="@+id/contacts_list_aizy"
+ android:layout_width="50dip"
+ android:layout_height="match_parent"
+ />
+
+ <LinearLayout
+ android:layout_width="0px"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:layout_weight="1"
+ >
+
+ <view
+ class="com.android.contacts.ContactEntryListView"
+ android:id="@android:id/list"
+ android:layout_width="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>
+</LinearLayout>
diff --git a/res/layout-finger/contacts_list_content_join.xml b/res/layout/contacts_list_content_join.xml
similarity index 91%
rename from res/layout-finger/contacts_list_content_join.xml
rename to res/layout/contacts_list_content_join.xml
index b50713b..b59c240 100644
--- a/res/layout-finger/contacts_list_content_join.xml
+++ b/res/layout/contacts_list_content_join.xml
@@ -61,10 +61,12 @@
</LinearLayout>
</LinearLayout>
- <ListView android:id="@android:id/list"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:fastScrollEnabled="true"
+ <view
+ class="com.android.contacts.ContactEntryListView"
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fastScrollEnabled="true"
/>
</LinearLayout>
diff --git a/res/layout-finger/contacts_list_empty.xml b/res/layout/contacts_list_empty.xml
similarity index 95%
rename from res/layout-finger/contacts_list_empty.xml
rename to res/layout/contacts_list_empty.xml
index 195da1e..d655899 100644
--- a/res/layout-finger/contacts_list_empty.xml
+++ b/res/layout/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_list_search_all_item.xml b/res/layout/contacts_list_search_all_item.xml
similarity index 100%
rename from res/layout-finger/contacts_list_search_all_item.xml
rename to res/layout/contacts_list_search_all_item.xml
diff --git a/res/layout-finger/contacts_list_search_results.xml b/res/layout/contacts_list_search_results.xml
similarity index 97%
rename from res/layout-finger/contacts_list_search_results.xml
rename to res/layout/contacts_list_search_results.xml
index 244ca80..7053cb6 100644
--- a/res/layout-finger/contacts_list_search_results.xml
+++ b/res/layout/contacts_list_search_results.xml
@@ -53,7 +53,7 @@
</LinearLayout>
<view
- class="com.android.contacts.PinnedHeaderListView"
+ class="com.android.contacts.ContactEntryListView"
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
diff --git a/res/layout-finger/contacts_list_show_all_item.xml b/res/layout/contacts_list_show_all_item.xml
similarity index 100%
rename from res/layout-finger/contacts_list_show_all_item.xml
rename to res/layout/contacts_list_show_all_item.xml
diff --git a/res/layout-finger/contacts_search_content.xml b/res/layout/contacts_search_content.xml
similarity index 76%
rename from res/layout-finger/contacts_search_content.xml
rename to res/layout/contacts_search_content.xml
index ae72376..d36ad00 100644
--- a/res/layout-finger/contacts_search_content.xml
+++ b/res/layout/contacts_search_content.xml
@@ -26,10 +26,11 @@
<include android:id="@+id/searchView"
layout="@layout/search_bar"/>
- <ListView
- android:id="@android:id/list"
+ <FrameLayout
+ android:id="@+id/list_container"
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,13 @@
<!-- 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"
+ android:visibility="gone"
+ />
+ <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/create_new_contact.xml b/res/layout/create_new_contact.xml
similarity index 100%
rename from res/layout-finger/create_new_contact.xml
rename to res/layout/create_new_contact.xml
diff --git a/res/layout-finger/dialer_activity.xml b/res/layout/dialer_activity.xml
similarity index 100%
rename from res/layout-finger/dialer_activity.xml
rename to res/layout/dialer_activity.xml
diff --git a/res/layout-finger/dialpad.xml b/res/layout/dialpad.xml
similarity index 100%
rename from res/layout-finger/dialpad.xml
rename to res/layout/dialpad.xml
diff --git a/res/layout-finger/dialpad_chooser_list_item.xml b/res/layout/dialpad_chooser_list_item.xml
similarity index 100%
rename from res/layout-finger/dialpad_chooser_list_item.xml
rename to res/layout/dialpad_chooser_list_item.xml
diff --git a/res/layout/directory_header.xml b/res/layout/directory_header.xml
new file mode 100644
index 0000000..53207e0
--- /dev/null
+++ b/res/layout/directory_header.xml
@@ -0,0 +1,58 @@
+<?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.
+-->
+
+<!-- Layout used for list section separators. -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="48dip"
+ android:background="@drawable/directory_bg"
+ >
+ <TextView
+ android:id="@+id/count"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_alignParentRight="true"
+ android:layout_marginRight="8dip"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorSecondary"
+ />
+ <TextView
+ android:id="@+id/display_name"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentBottom="true"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_marginLeft="6dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ />
+ <TextView
+ android:id="@+id/directory_type"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="6dip"
+ android:layout_marginBottom="-4dip"
+ android:layout_above="@id/display_name"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ />
+
+</RelativeLayout>
diff --git a/res/layout-finger/display_child.xml b/res/layout/display_child.xml
similarity index 100%
rename from res/layout-finger/display_child.xml
rename to res/layout/display_child.xml
diff --git a/res/layout-finger/display_group.xml b/res/layout/display_group.xml
similarity index 100%
rename from res/layout-finger/display_group.xml
rename to res/layout/display_group.xml
diff --git a/res/layout-finger/display_options_phones_only.xml b/res/layout/display_options_phones_only.xml
similarity index 100%
rename from res/layout-finger/display_options_phones_only.xml
rename to res/layout/display_options_phones_only.xml
diff --git a/res/layout-finger/edit_contact_entry_voicemail.xml b/res/layout/edit_contact_entry_voicemail.xml
similarity index 100%
rename from res/layout-finger/edit_contact_entry_voicemail.xml
rename to res/layout/edit_contact_entry_voicemail.xml
diff --git a/res/layout-finger/edit_divider.xml b/res/layout/edit_divider.xml
similarity index 100%
rename from res/layout-finger/edit_divider.xml
rename to res/layout/edit_divider.xml
diff --git a/res/layout-finger/edit_phonetic_name.xml b/res/layout/edit_phonetic_name.xml
similarity index 100%
rename from res/layout-finger/edit_phonetic_name.xml
rename to res/layout/edit_phonetic_name.xml
diff --git a/res/layout-finger/empty.xml b/res/layout/empty.xml
similarity index 100%
rename from res/layout-finger/empty.xml
rename to res/layout/empty.xml
diff --git a/res/layout/footer_panel.xml b/res/layout/footer_panel.xml
new file mode 100644
index 0000000..2625a43
--- /dev/null
+++ b/res/layout/footer_panel.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/footer"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ style="@android:style/ButtonBar"
+>
+
+ <Button android:id="@+id/done"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/menu_done"
+ />
+
+ <Button android:id="@+id/revert"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/menu_doNotSave"
+ />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-finger/horizontal_divider.xml b/res/layout/horizontal_divider.xml
similarity index 100%
rename from res/layout-finger/horizontal_divider.xml
rename to res/layout/horizontal_divider.xml
diff --git a/res/layout/item_contact_editor.xml b/res/layout/item_contact_editor.xml
index f5e7bd3..57d641f 100644
--- a/res/layout/item_contact_editor.xml
+++ b/res/layout/item_contact_editor.xml
@@ -14,7 +14,7 @@
limitations under the License.
-->
-<!-- placed inside act_edit as tabcontent -->
+<!-- placed inside act_edit -->
<com.android.contacts.ui.widget.ContactEditorView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
@@ -22,14 +22,6 @@
android:orientation="horizontal"
>
- <!-- Left side color bar -->
- <ImageView
- android:id="@+id/color_bar"
- android:layout_width="4dip"
- android:layout_height="match_parent"
- android:visibility="gone"
- />
-
<!-- The content -->
<LinearLayout
android:layout_width="0dip"
@@ -116,65 +108,28 @@
android:layout_marginBottom="4dip"
layout="@layout/item_generic_editor" />
- <TextView android:id="@+id/read_only_name"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="6dip"
- android:layout_marginBottom="6dip"
- android:layout_marginLeft="10dip"
-
- android:textAppearance="?android:attr/textAppearanceLarge"
- />
-
<LinearLayout
- android:id="@+id/sect_general"
+ android:id="@+id/sect_fields"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
/>
- <View android:id="@+id/head_secondary_divider"
+ <View
android:layout_width="match_parent"
android:layout_height="1px"
- android:background="?android:attr/listDivider" />
-
- <TextView
- android:id="@+id/head_secondary"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
-
- android:gravity="center_vertical"
- android:minHeight="?android:attr/listPreferredItemHeight"
- android:text="@string/edit_secondary_collapse"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:textColor="@color/kind_title"
- android:singleLine="true"
- android:ellipsize="marquee"
- android:focusable="true"
- android:clickable="true"
- android:paddingLeft="10dip"
- android:drawablePadding="10dip" />
-
- <LinearLayout
- android:id="@+id/sect_secondary"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical" />
-
- <TextView
- android:id="@+id/edit_read_only"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="10dip"
- android:layout_marginBottom="10dip"
- android:layout_marginLeft="10dip"
-
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textColor="?android:attr/textColorPrimary"
- android:drawableLeft="@android:drawable/ic_dialog_alert"
- android:drawablePadding="10dip"
+ android:layout_alignParentBottom="true"
+ android:background="?android:attr/listDivider"
/>
+ <Button
+ android:id="@+id/button_add_field"
+ android:text="@string/add_field"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:layout_marginTop="10dip"
+ />
</LinearLayout>
</com.android.contacts.ui.widget.ContactEditorView>
diff --git a/res/layout/item_generic_editor.xml b/res/layout/item_generic_editor.xml
index e672eba..329ad28 100644
--- a/res/layout/item_generic_editor.xml
+++ b/res/layout/item_generic_editor.xml
@@ -48,21 +48,12 @@
android:gravity="center_vertical" />
<ImageButton
- android:id="@+id/edit_more"
+ android:id="@+id/edit_more_or_less"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignBottom="@id/edit_fields"
android:visibility="gone"
- style="@style/MoreButton" />
-
- <ImageButton
- android:id="@+id/edit_less"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentRight="true"
- android:layout_alignBottom="@id/edit_fields"
- android:visibility="gone"
- style="@style/LessButton" />
+ style="@style/EmptyButton" />
</com.android.contacts.ui.widget.GenericEditorView>
diff --git a/res/layout/item_kind_section.xml b/res/layout/item_kind_section.xml
index d1dec5e..a78896b 100644
--- a/res/layout/item_kind_section.xml
+++ b/res/layout/item_kind_section.xml
@@ -25,7 +25,7 @@
<View
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="1px"
android:background="?android:attr/listDivider" />
<LinearLayout
diff --git a/res/layout/list_edit_item_field_and_type.xml b/res/layout/list_edit_item_field_and_type.xml
new file mode 100644
index 0000000..ac43ceb
--- /dev/null
+++ b/res/layout/list_edit_item_field_and_type.xml
@@ -0,0 +1,47 @@
+<?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.
+ */
+-->
+
+<com.android.contacts.views.editor.view.FieldAndTypeView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal"
+ android:paddingLeft="9dip"
+ android:gravity="center_vertical"
+ android:background="@drawable/edit_rawcontact_bg"
+>
+
+ <TextView android:id="@+id/caption"
+ android:layout_width="60dip"
+ android:layout_height="wrap_content"
+ />
+
+ <EditText android:id="@+id/field"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ />
+
+ <Button android:id="@+id/type"
+ android:layout_width="100dip"
+ android:layout_height="wrap_content"
+ />
+
+</com.android.contacts.views.editor.view.FieldAndTypeView>
diff --git a/res/layout/list_edit_item_footer.xml b/res/layout/list_edit_item_footer.xml
new file mode 100644
index 0000000..4ad3638
--- /dev/null
+++ b/res/layout/list_edit_item_footer.xml
@@ -0,0 +1,49 @@
+<?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.
+ */
+-->
+
+<com.android.contacts.views.editor.view.FooterView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal"
+ android:paddingLeft="9dip"
+ android:gravity="center_vertical"
+ android:background="@drawable/edit_rawcontact_bottom"
+>
+
+ <Button android:id="@+id/add_information"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/add_field"
+ />
+
+ <Button android:id="@+id/separate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/menu_splitAggregate"
+ />
+
+ <Button android:id="@+id/delete"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/edit_delete_rawcontact"
+ />
+
+</com.android.contacts.views.editor.view.FooterView>
diff --git a/res/layout/list_edit_item_header.xml b/res/layout/list_edit_item_header.xml
new file mode 100644
index 0000000..aa33a78
--- /dev/null
+++ b/res/layout/list_edit_item_header.xml
@@ -0,0 +1,45 @@
+<?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.
+ */
+-->
+
+<com.android.contacts.views.editor.view.HeaderView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal"
+ android:background="@drawable/edit_rawcontact_top"
+ android:gravity="center"
+>
+ <ImageView android:id="@+id/logo"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="2dip"
+ android:layout_centerVertical="true"
+ />
+ <TextView android:id="@+id/caption"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:paddingLeft="14dip"
+ android:paddingRight="14dip"
+ android:gravity="center"
+ android:scaleType="center"
+ android:background="@android:drawable/list_selector_background"
+ />
+</com.android.contacts.views.editor.view.HeaderView>
diff --git a/res/layout/list_edit_item_organization.xml b/res/layout/list_edit_item_organization.xml
new file mode 100644
index 0000000..588a2a2
--- /dev/null
+++ b/res/layout/list_edit_item_organization.xml
@@ -0,0 +1,52 @@
+<?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.
+ */
+-->
+
+<com.android.contacts.views.editor.view.OrganizationView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal"
+ android:paddingLeft="9dip"
+ android:gravity="center_vertical"
+ android:background="@drawable/edit_rawcontact_bg">
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <TextView android:id="@+id/caption"
+ android:layout_width="60dip"
+ android:layout_height="wrap_content" />
+ <Button android:id="@+id/type"
+ android:layout_width="100dip"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="0px"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ <EditText android:id="@+id/company_field"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ <EditText android:id="@+id/title_field"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+</com.android.contacts.views.editor.view.OrganizationView>
diff --git a/res/layout/list_edit_item_photo.xml b/res/layout/list_edit_item_photo.xml
new file mode 100644
index 0000000..9e7951f
--- /dev/null
+++ b/res/layout/list_edit_item_photo.xml
@@ -0,0 +1,72 @@
+<?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.
+ */
+-->
+
+<com.android.contacts.views.editor.view.PhotoView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal"
+ android:paddingLeft="9dip"
+ android:gravity="center_vertical"
+ android:background="@drawable/edit_rawcontact_bg"
+>
+
+ <ImageView android:id="@+id/photo"
+ android:layout_width="50dip"
+ android:layout_height="50dip"
+ android:layout_marginLeft="5dip"
+ android:scaleType="centerInside"
+ />
+
+ <!-- Padding to make the icons right aligned -->
+ <LinearLayout
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ />
+
+ <ImageView android:id="@+id/action_icon"
+ android:layout_width="30dip"
+ android:layout_height="30dip"
+ android:layout_marginLeft="14dip"
+ android:layout_marginRight="14dip"
+ android:gravity="center"
+ android:scaleType="centerInside"
+ />
+
+ <View android:id="@+id/divider"
+ android:layout_width="1px"
+ android:layout_height="match_parent"
+ android:layout_marginTop="5dip"
+ android:layout_marginBottom="5dip"
+ android:background="@drawable/divider_vertical_dark"
+ />
+
+ <ImageView android:id="@+id/secondary_action_button"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:paddingLeft="14dip"
+ android:paddingRight="14dip"
+ android:gravity="center"
+ android:scaleType="center"
+ android:background="@android:drawable/list_selector_background"
+ />
+</com.android.contacts.views.editor.view.PhotoView>
diff --git a/res/layout/list_edit_item_simple_or_structured.xml b/res/layout/list_edit_item_simple_or_structured.xml
new file mode 100644
index 0000000..240f26d
--- /dev/null
+++ b/res/layout/list_edit_item_simple_or_structured.xml
@@ -0,0 +1,48 @@
+<?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.
+ */
+-->
+
+<com.android.contacts.views.editor.view.SimpleOrStructuredView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal"
+ android:paddingLeft="9dip"
+ android:gravity="center_vertical"
+ android:background="@drawable/edit_rawcontact_bg"
+>
+
+ <TextView android:id="@+id/caption"
+ android:layout_width="60dip"
+ android:layout_height="wrap_content"
+ />
+
+ <EditText android:id="@+id/field"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ />
+
+ <Button android:id="@+id/structuredEditorButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/edit_structured_editor_button"
+ />
+
+</com.android.contacts.views.editor.view.SimpleOrStructuredView>
diff --git a/res/layout/list_edit_item_single_field.xml b/res/layout/list_edit_item_single_field.xml
new file mode 100644
index 0000000..4ffc0d7
--- /dev/null
+++ b/res/layout/list_edit_item_single_field.xml
@@ -0,0 +1,42 @@
+<?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.
+ */
+-->
+
+<com.android.contacts.views.editor.view.SingleFieldView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal"
+ android:paddingLeft="9dip"
+ android:gravity="center_vertical"
+ android:background="@drawable/edit_rawcontact_bg"
+>
+
+ <TextView android:id="@+id/caption"
+ android:layout_width="60dip"
+ android:layout_height="wrap_content"
+ />
+
+ <EditText android:id="@+id/field"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ />
+
+</com.android.contacts.views.editor.view.SingleFieldView>
diff --git a/res/layout-finger/list_item_text_icons.xml b/res/layout/list_item_text_icons.xml
similarity index 100%
rename from res/layout-finger/list_item_text_icons.xml
rename to res/layout/list_item_text_icons.xml
diff --git a/res/layout-finger/list_section.xml b/res/layout/list_section.xml
similarity index 100%
rename from res/layout-finger/list_section.xml
rename to res/layout/list_section.xml
diff --git a/res/layout-finger/list_separator.xml b/res/layout/list_separator.xml
similarity index 100%
rename from res/layout-finger/list_separator.xml
rename to res/layout/list_separator.xml
diff --git a/res/layout-finger/phone_disambig_item.xml b/res/layout/phone_disambig_item.xml
similarity index 100%
rename from res/layout-finger/phone_disambig_item.xml
rename to res/layout/phone_disambig_item.xml
diff --git a/res/layout-finger/contacts_list_content.xml b/res/layout/pinned_header_list_demo.xml
similarity index 81%
rename from res/layout-finger/contacts_list_content.xml
rename to res/layout/pinned_header_list_demo.xml
index 36c03ce..9a26e98 100644
--- a/res/layout-finger/contacts_list_content.xml
+++ b/res/layout/pinned_header_list_demo.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 The Android Open Source Project
+<!-- 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.
@@ -14,7 +14,7 @@
limitations under the License.
-->
-<LinearLayout
+<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pinned_header_list_layout"
android:layout_width="match_parent"
@@ -23,13 +23,11 @@
>
<view
- class="com.android.contacts.PinnedHeaderListView"
+ class="com.android.contacts.widget.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"/>
-
</LinearLayout>
diff --git a/res/layout-finger/preference_with_more_button.xml b/res/layout/preference_with_more_button.xml
similarity index 100%
rename from res/layout-finger/preference_with_more_button.xml
rename to res/layout/preference_with_more_button.xml
diff --git a/res/layout-finger/quickcontact.xml b/res/layout/quickcontact.xml
similarity index 100%
rename from res/layout-finger/quickcontact.xml
rename to res/layout/quickcontact.xml
diff --git a/res/layout-finger/quickcontact_header_large.xml b/res/layout/quickcontact_header_large.xml
similarity index 100%
rename from res/layout-finger/quickcontact_header_large.xml
rename to res/layout/quickcontact_header_large.xml
diff --git a/res/layout-finger/quickcontact_header_med.xml b/res/layout/quickcontact_header_med.xml
similarity index 100%
rename from res/layout-finger/quickcontact_header_med.xml
rename to res/layout/quickcontact_header_med.xml
diff --git a/res/layout-finger/quickcontact_header_small.xml b/res/layout/quickcontact_header_small.xml
similarity index 100%
rename from res/layout-finger/quickcontact_header_small.xml
rename to res/layout/quickcontact_header_small.xml
diff --git a/res/layout-finger/quickcontact_item.xml b/res/layout/quickcontact_item.xml
similarity index 100%
rename from res/layout-finger/quickcontact_item.xml
rename to res/layout/quickcontact_item.xml
diff --git a/res/layout-finger/quickcontact_resolve_item.xml b/res/layout/quickcontact_resolve_item.xml
similarity index 100%
rename from res/layout-finger/quickcontact_resolve_item.xml
rename to res/layout/quickcontact_resolve_item.xml
diff --git a/res/layout-finger/recent_calls.xml b/res/layout/recent_calls.xml
similarity index 100%
rename from res/layout-finger/recent_calls.xml
rename to res/layout/recent_calls.xml
diff --git a/res/layout-finger/recent_calls_list_child_item.xml b/res/layout/recent_calls_list_child_item.xml
similarity index 93%
rename from res/layout-finger/recent_calls_list_child_item.xml
rename to res/layout/recent_calls_list_child_item.xml
index 14eb24d..527e259 100644
--- a/res/layout-finger/recent_calls_list_child_item.xml
+++ b/res/layout/recent_calls_list_child_item.xml
@@ -21,7 +21,7 @@
android:background="@drawable/list_item_background_secondary"
>
- <com.android.contacts.ui.widget.DontPressWithParentImageView android:id="@+id/call_icon"
+ <ImageView android:id="@+id/call_icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingLeft="14dip"
diff --git a/res/layout-finger/recent_calls_list_group_item.xml b/res/layout/recent_calls_list_group_item.xml
similarity index 97%
rename from res/layout-finger/recent_calls_list_group_item.xml
rename to res/layout/recent_calls_list_group_item.xml
index 2d3e7af..5b4cf21 100644
--- a/res/layout-finger/recent_calls_list_group_item.xml
+++ b/res/layout/recent_calls_list_group_item.xml
@@ -20,7 +20,7 @@
android:paddingLeft="7dip"
>
- <com.android.contacts.ui.widget.DontPressWithParentImageView android:id="@+id/call_icon"
+ <ImageView android:id="@+id/call_icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingLeft="14dip"
diff --git a/res/layout-finger/recent_calls_list_item.xml b/res/layout/recent_calls_list_item.xml
similarity index 93%
rename from res/layout-finger/recent_calls_list_item.xml
rename to res/layout/recent_calls_list_item.xml
index 8efa23c..2c519d6 100644
--- a/res/layout-finger/recent_calls_list_item.xml
+++ b/res/layout/recent_calls_list_item.xml
@@ -20,7 +20,7 @@
android:paddingLeft="7dip"
>
- <com.android.contacts.ui.widget.DontPressWithParentImageView android:id="@+id/call_icon"
+ <ImageView android:id="@+id/call_icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingLeft="14dip"
diff --git a/res/layout-finger/recent_calls_list_item_layout.xml b/res/layout/recent_calls_list_item_layout.xml
similarity index 100%
rename from res/layout-finger/recent_calls_list_item_layout.xml
rename to res/layout/recent_calls_list_item_layout.xml
diff --git a/res/layout-finger/search_bar.xml b/res/layout/search_bar.xml
similarity index 97%
rename from res/layout-finger/search_bar.xml
rename to res/layout/search_bar.xml
index d322948..304c35d 100644
--- a/res/layout-finger/search_bar.xml
+++ b/res/layout/search_bar.xml
@@ -52,7 +52,7 @@
android:scaleType="centerInside" />
<view
- class="com.android.contacts.SearchEditText"
+ class="com.android.contacts.widget.SearchEditText"
android:id="@+id/search_src_text"
android:layout_height="wrap_content"
android:layout_width="0dip"
diff --git a/res/layout-finger/set_primary_checkbox.xml b/res/layout/set_primary_checkbox.xml
similarity index 100%
rename from res/layout-finger/set_primary_checkbox.xml
rename to res/layout/set_primary_checkbox.xml
diff --git a/res/layout-finger/split_aggregate_list_item.xml b/res/layout/split_aggregate_list_item.xml
similarity index 100%
rename from res/layout-finger/split_aggregate_list_item.xml
rename to res/layout/split_aggregate_list_item.xml
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/layout-finger/tab_account_name.xml b/res/layout/tab_account_name.xml
similarity index 100%
rename from res/layout-finger/tab_account_name.xml
rename to res/layout/tab_account_name.xml
diff --git a/res/layout-finger/tab_indicator.xml b/res/layout/tab_indicator.xml
similarity index 100%
rename from res/layout-finger/tab_indicator.xml
rename to res/layout/tab_indicator.xml
diff --git a/res/layout-finger/tab_layout.xml b/res/layout/tab_layout.xml
similarity index 100%
rename from res/layout-finger/tab_layout.xml
rename to res/layout/tab_layout.xml
diff --git a/res/layout-finger/tab_left_arrow.xml b/res/layout/tab_left_arrow.xml
similarity index 100%
rename from res/layout-finger/tab_left_arrow.xml
rename to res/layout/tab_left_arrow.xml
diff --git a/res/layout-finger/tab_right_arrow.xml b/res/layout/tab_right_arrow.xml
similarity index 100%
rename from res/layout-finger/tab_right_arrow.xml
rename to res/layout/tab_right_arrow.xml
diff --git a/res/layout-finger/total_contacts.xml b/res/layout/total_contacts.xml
similarity index 100%
rename from res/layout-finger/total_contacts.xml
rename to res/layout/total_contacts.xml
diff --git a/res/layout-finger/twelve_key_dialer.xml b/res/layout/twelve_key_dialer.xml
similarity index 100%
rename from res/layout-finger/twelve_key_dialer.xml
rename to res/layout/twelve_key_dialer.xml
diff --git a/res/layout/two_pane_activity.xml b/res/layout/two_pane_activity.xml
new file mode 100644
index 0000000..407360b
--- /dev/null
+++ b/res/layout/two_pane_activity.xml
@@ -0,0 +1,45 @@
+<?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/two_pane_activity"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <include android:id="@+id/searchView"
+ layout="@layout/search_bar"/>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <fragment android:name="com.android.contacts.list.DefaultContactBrowseListFragment"
+ android:id="@+id/two_pane_list"
+ android:layout_width="0px"
+ android:layout_height="match_parent"
+ android:layout_weight="1" />
+
+ <!-- Holder for detail- or editor-fragment. -->
+ <LinearLayout
+ android:id="@+id/two_pane_right_view"
+ android:orientation="horizontal"
+ android:layout_width="0px"
+ android:layout_height="match_parent"
+ android:layout_weight="1" />
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/layout-finger/voicemail_dial_delete.xml b/res/layout/voicemail_dial_delete.xml
similarity index 100%
rename from res/layout-finger/voicemail_dial_delete.xml
rename to res/layout/voicemail_dial_delete.xml
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/menu/search.xml b/res/menu/search.xml
new file mode 100644
index 0000000..4e2e8e3
--- /dev/null
+++ b/res/menu/search.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/menu_search"
+ android:icon="@android:drawable/ic_menu_search"
+ android:title="@string/menu_search" />
+
+</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/donottranslate_config.xml b/res/values/donottranslate_config.xml
index f1c6951..cad2a63 100644
--- a/res/values/donottranslate_config.xml
+++ b/res/values/donottranslate_config.xml
@@ -53,7 +53,7 @@
<string name="config_export_vcard_type" translatable="false">default</string>
<!-- Directory in which exported VCard file is stored -->
- <string name="config_export_dir" translatable="false">/sdcard</string>
+ <string name="config_export_dir" translatable="false">/mnt/sdcard</string>
<!-- Prefix of exported VCard file -->
<string name="config_export_file_prefix" translatable="false"></string>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index ceb10f8..49f1c0b 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -14,12 +14,33 @@
limitations under the License.
-->
<resources>
- <!-- The EditText for entries in the EditContactActivity -->
+ <!-- Dialogs in ContactDetailFragment -->
+ <item type="id" name="detail_dialog_confirm_delete" />
+ <item type="id" name="detail_dialog_confirm_readonly_delete" />
+ <item type="id" name="detail_dialog_confirm_multiple_delete" />
+ <item type="id" name="detail_dialog_confirm_readonly_hide" />
+
+ <!-- The EditText for entries in the ContactEditFragment -->
<item type="id" name="data"/>
<item type="id" name="header_phones"/>
<item type="id" name="dialog_sync_add"/>
<item type="id" name="dialog_import_export"/>
+ <!-- Dialogs in ContactEditFragment -->
+ <item type="id" name="edit_dialog_confirm_delete"/>
+ <item type="id" name="edit_dialog_confirm_readonly_delete"/>
+ <item type="id" name="edit_dialog_confirm_multiple_delete"/>
+ <item type="id" name="edit_dialog_confirm_readonly_hide"/>
+ <item type="id" name="edit_dialog_pick_photo"/>
+ <item type="id" name="edit_dialog_split"/>
+ <item type="id" name="edit_dialog_select_account"/>
+ <item type="id" name="edit_dialog_add_information"/>
+
+ <!-- Request Codes (startActivityForResult) -->
+ <item type="id" name="edit_request_code_join"/>
+ <item type="id" name="edit_request_code_camera_with_data"/>
+ <item type="id" name="edit_request_code_photo_picked_with_data"/>
+
<!-- For ImportVCardActivity -->
<item type="id" name="dialog_searching_vcard"/>
<item type="id" name="dialog_sdcard_not_found"/>
@@ -27,7 +48,7 @@
<item type="id" name="dialog_select_import_type"/>
<item type="id" name="dialog_select_one_vcard"/>
<item type="id" name="dialog_select_multiple_vcard"/>
- <item type="id" name="dialog_reading_vcard"/>
+ <item type="id" name="dialog_cache_vcard"/>
<item type="id" name="dialog_io_exception"/>
<item type="id" name="dialog_error_with_message"/>
@@ -37,7 +58,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 facf34c..3c8cd18 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -68,6 +68,12 @@
editing a contact. This string represents the built in way to edit the contact. -->
<string name="editContactDescription">Edit contact</string>
+ <!-- The caption in the edit-email screen -->
+ <string name="edit_email_caption">Email address:</string>
+
+ <!-- The caption of the custom type -->
+ <string name="edit_email_type_caption">Type:</string>
+
<!-- The description presented to the user in the Intent choose when there are multiple activities that allow
creating a new contact. This string represents the built in way to create the contact. -->
<string name="insertContactDescription">Create contact</string>
@@ -329,17 +335,17 @@
<!-- Displayed at the top of the contacts showing the total number of contacts found when "Only contacts with phones" not selected -->
<plurals name="listFoundAllContacts">
- <item quantity="one">Found 1 contact</item>
- <item quantity="other">Found <xliff:g id="count">%d</xliff:g> contacts</item>
+ <item quantity="one">1 found</item>
+ <item quantity="other"><xliff:g id="count">%d</xliff:g> found</item>
</plurals>
<!-- Displayed at the top of the contacts showing the zero total number of contacts found when "Only contacts with phones" not selected -->
- <string name="listFoundAllContactsZero">Contact not found</string>
+ <string name="listFoundAllContactsZero">Not found</string>
<!-- Displayed at the top of the contacts showing the total number of contacts found when typing search query -->
<plurals name="searchFoundContacts">
- <item quantity="one">1 contact</item>
- <item quantity="other"><xliff:g id="count">%d</xliff:g> contacts</item>
+ <item quantity="one">1 found</item>
+ <item quantity="other"><xliff:g id="count">%d</xliff:g> found</item>
</plurals>
<!-- The description text for the contacts tab. Space is limited for this string, so the shorter the better -->
@@ -428,7 +434,7 @@
<string name="noContactsHelpText">"You don't have any contacts to display.\n\nTo add contacts, press <font fgcolor="#ffffffff"><b>Menu</b></font> and touch:\n
\n<li><font fgcolor="#ffffffff"><b>Accounts</b></font> to add or configure an account with contacts you can sync to the phone\n</li>
\n<li><font fgcolor="#ffffffff"><b>New contact</b></font> to create a new contact from scratch\n</li>
- \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font>\n</li>"
+ \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font> to import contacts from your SIM or SD card\n</li>"
</string>
<!-- Displayed full screen when the user has no contacts and they are displaying the My Contacts group, and contact syncing is enabled -->
@@ -436,14 +442,14 @@
\n<li><font fgcolor="#ffffffff"><b>Accounts</b></font> to add or configure an account with contacts you can sync to the phone\n</li>
\n<li><font fgcolor="#ffffffff"><b>Display options</b></font> to change which contacts are visible\n</li>
\n<li><font fgcolor="#ffffffff"><b>New contact</b></font> to create a new contact from scratch\n</li>
- \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font>\n</li>"
+ \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font> to import contacts from your SIM or SD card\n</li>"
</string>
<!-- Displayed full screen when the user has no contacts and they are displaying the My Contacts group, and contact syncing is disabled, and there is no sim card (cdma)-->
<string name="noContactsNoSimHelpText">"You don't have any contacts to display.\n\nTo add contacts, press <font fgcolor="#ffffffff"><b>Menu</b></font> and touch:\n
\n<li><font fgcolor="#ffffffff"><b>Accounts</b></font> to add or configure an account with contacts you can sync to the phone\n</li>
\n<li><font fgcolor="#ffffffff"><b>New contact</b></font> to create a new contact from scratch\n</li>
- \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font>\n</li>"
+ \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font> to import contacts from your SD card\n</li>"
</string>
<!-- Displayed full screen when the user has no contacts and they are displaying the My Contacts group, and contact syncing is enabled, and there is no sim card (cdma) -->
@@ -451,7 +457,7 @@
\n<li><font fgcolor="#ffffffff"><b>Accounts</b></font> to add or configure an account with contacts you can sync to the phone\n</li>
\n<li><font fgcolor="#ffffffff"><b>Display options</b></font> to change which contacts are visible\n</li>
\n<li><font fgcolor="#ffffffff"><b>New contact</b></font> to create a new contact from scratch\n</li>
- \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font>\n</li>"
+ \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font> to import contacts from your SD card\n</li>"
</string>
<!-- Displayed full screen when the user has no favorites and they are displaying the favorites tab -->
@@ -516,7 +522,7 @@
<string name="returnCall">Return call</string>
<!-- A nicely formatted call duration displayed when viewing call details. For example "42 mins 28 secs" -->
- <string name="callDetailsDurationFormat"><xliff:g id="minutes" example="42">%1$s</xliff:g> mins <xliff:g id="seconds" example="28">%2$s</xliff:g> secs</string>
+ <string name="callDetailsDurationFormat"><xliff:g id="minutes" example="42">%s</xliff:g> mins <xliff:g id="seconds" example="28">%s</xliff:g> secs</string>
<!-- A list separator for the Favorites tab indicating that items below it are frequently contacted contacts rather than starred contacts -->
<string name="favoritesFrquentSeparator">Frequently contacted</string>
@@ -662,7 +668,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 -->
@@ -692,13 +698,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>
@@ -706,6 +712,10 @@
emitted some I/O error. Exact reason will be appended by the system. -->
<string name="fail_reason_io_error">I/O Error</string>
+ <!-- Failure reason show when Contacts app (especially vCard importer) encountered
+ low memory problem and could not proceed its import procedure. -->
+ <string name="fail_reason_low_memory_during_import">Memory is insufficient (the file may be too large)</string>
+
<!-- The failed reason shown when vCard parser was not able to be parsed by the current vCard
implementation. This might happen even when the input vCard is completely valid, though
we believe it is rather rare in the actual world. -->
@@ -721,6 +731,9 @@
(with extension ".vcf" in SDCard.) -->
<string name="fail_reason_no_vcard_file">No vCard file found on the SD card</string>
+ <!-- Fail reason shown when vCard importer failed to look over meta information stored in vCard file(s). -->
+ <string name="fail_reason_failed_to_collect_vcard_meta_info">Failed to collect meta information of given vCard file(s).</string>
+
<!-- The failed reason shown when the import of some of vCard files failed during multiple vCard
files import. It includes the case where all files were failed to be imported. -->
<string name="fail_reason_failed_to_read_files">One or more files failed to be imported (%s).</string>
@@ -728,28 +741,43 @@
<!-- 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">%1$s</xliff:g>\n<xliff:g id="filename" example="foo.vcf">%2$s</xliff:g></string>
+ <!-- The title shown when vCard importer is caching files to be imported into local temporary
+ data storage. -->
+ <string name="caching_vcard_title">Caching vCard(s) to local temporary storage</string>
- <!-- Dialog title shown when reading VCard data -->
- <string name="reading_vcard_title">Reading vCard</string>
+ <!-- The message shown when vCard importer is caching files to be imported into local temporary
+ data storage. -->
+ <string name="caching_vcard_message">Importer is caching vCard(s) to local temporary storage. Actual import will start soon.</string>
- <!-- Dialog message shown when reading a VCard file -->
- <string name="reading_vcard_message">Reading vCard file(s)</string>
+ <!-- The message shown while importing 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 failed -->
- <string name="reading_vcard_failed_title">Reading of vCard data has failed</string>
+ <!-- Dialog title shown when reading vCard data -->
+ <string name="reading_vcard_title">Reading vCard(s)</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">%1$s</xliff:g> of <xliff:g id="total_number">%2$s</xliff:g> contacts</string>
+ <!-- Dialog title shown when reading vCard data failed -->
+ <string name="reading_vcard_failed_title">Failed to Read 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">%1$s</xliff:g> of <xliff:g id="total_number">%2$s</xliff:g> files</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>
+
+ <!-- The title shown when reading vCard is canceled (probably by a user) -->
+ <string name="importing_vcard_finished_title">Finished importing vCard</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/export. -->
+ <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>
@@ -777,6 +805,12 @@
mention it here. -->
<string name="fail_reason_too_long_filename">Required filename is too long (\"<xliff:g id="filename">%s</xliff:g>\")</string>
+ <!-- The message shown when vCard importer started running. -->
+ <string name="vcard_exporter_start_message">vCard exporter started.</string>
+
+ <!-- The title shown when reading vCard is canceled (probably by a user) -->
+ <string name="exporting_vcard_finished_title">Finished exporting vCard</string>
+
<!-- Dialog title shown when the application is exporting contact data outside -->
<string name="exporting_contact_list_title">Exporting contact data</string>
@@ -807,10 +841,10 @@
<!-- The failed reason shown when vCard importer/exporter could not open the file
specified by a user. The file name should be in the message. -->
- <string name="fail_reason_could_not_open_file">Could not open \"<xliff:g id="file_name">%1$s</xliff:g>\": <xliff:g id="exact_reason">%2$s</xliff:g></string>
+ <string name="fail_reason_could_not_open_file">Could not open \"<xliff:g id="file_name">%s</xliff:g>\": <xliff:g id="exact_reason">%s</xliff:g></string>
<!-- Message in progress bar while exporting contact list to a 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="exporting_contact_list_progress"><xliff:g id="current_number">%1$s</xliff:g> of <xliff:g id="total_number">%2$s</xliff:g> contacts</string>
+ <string name="exporting_contact_list_progress"><xliff:g id="current_number">%s</xliff:g> of <xliff:g id="total_number">%s</xliff:g> contacts</string>
<!-- The string used to describe Contacts as a searchable item within system search settings. -->
<string name="search_settings_description">Names of your contacts</string>
@@ -1077,6 +1111,9 @@
<!-- String describing which account a contact came from when editing it -->
<string name="from_account_format">from <xliff:g id="source" example="user@gmail.com">%1$s</xliff:g></string>
+ <!-- String describing both account type and account name -->
+ <string name="account_type_and_name"><xliff:g id="source" example="Gmail">%1$s</xliff:g> contact from <xliff:g id="source" example="user@gmail.com">%2$s</xliff:g></string>
+
<!-- Checkbox asking the user if they want to display a particular photo for a contact -->
<string name="use_photo_as_primary">Use this photo</string>
@@ -1138,4 +1175,73 @@
<!-- 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>
+
+ <!-- The add field button shown in the editor under each editable Raw Contact -->
+ <string name="add_field">Add information</string>
+
+ <!-- Attbution of a contact status update, when the time of update is unknown -->
+ <string name="contact_status_update_attribution">via <xliff:g id="source" example="Google Talk">%1$s</xliff:g></string>
+
+ <!-- Attbution of a contact status update, when the time of update is known -->
+ <string name="contact_status_update_attribution_with_date"><xliff:g id="date" example="3 hours ago">%1$s</xliff:g> via <xliff:g id="source" example="Google Talk">%2$s</xliff:g></string>
+
+ <!-- String describing the Edit push button
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_edit">edit</string>
+
+ <!-- String describing the Star/Favorite checkbox
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_star">favorite</string>
+
+ <!-- The title of the Edit-Contact screen -->
+ <string name="edit_contact">Edit contact</string>
+
+ <!-- Shows how many contacts have been merged. The value 1 is not shown but should be translated
+ anyway if we change our mind later -->
+ <plurals name="merge_info">
+ <item quantity="one">not merged</item>
+ <item quantity="other">merged from <xliff:g id="count">%0$d</xliff:g> sources</item>
+ </plurals>
+
+ <!-- Command to delete a RawContact (a Fragment of a Contact) -->
+ <string name="edit_delete_rawcontact">Delete</string>
+
+ <!-- Shown in a Toast to indicate an error while trying to save the Data -->
+ <string name="edit_error_saving">Error saving</string>
+
+ <!-- Text in the editor to show the structured editor -->
+ <string name="edit_structured_editor_button">...</string>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ad4f4f6..93ae91d 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -24,6 +24,10 @@
<item name="android:windowAnimationStyle">@style/ContactsSearchAnimation</item>
</style>
+ <style name="EmptyButton">
+ <item name="android:background">@drawable/btn_circle</item>
+ </style>
+
<style name="MinusButton">
<item name="android:background">@drawable/btn_circle</item>
<item name="android:src">@drawable/ic_btn_round_minus</item>
diff --git a/src/com/android/contacts/CallContactActivity.java b/src/com/android/contacts/CallContactActivity.java
new file mode 100644
index 0000000..181e9b7
--- /dev/null
+++ b/src/com/android/contacts/CallContactActivity.java
@@ -0,0 +1,57 @@
+/*
+ * 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.list.CallOrSmsInitiator;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.DialogInterface.OnDismissListener;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+
+/**
+ * An interstitial activity used when the user selects a QSB search suggestion using
+ * a call button.
+ */
+public class CallContactActivity extends Activity implements OnDismissListener {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Uri data = getIntent().getData();
+ if (data == null) {
+ finish();
+ }
+
+ if (Contacts.CONTENT_ITEM_TYPE.equals(getContentResolver().getType(data))) {
+ CallOrSmsInitiator initiator = new CallOrSmsInitiator(this);
+ initiator.setOnDismissListener(this);
+ initiator.initiateCall(data);
+ } else {
+ startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED, data));
+ finish();
+ }
+ }
+
+ public void onDismiss(DialogInterface dialog) {
+ finish();
+ }
+}
diff --git a/src/com/android/contacts/Collapser.java b/src/com/android/contacts/Collapser.java
index 3872dfd..d072dce 100644
--- a/src/com/android/contacts/Collapser.java
+++ b/src/com/android/contacts/Collapser.java
@@ -16,7 +16,6 @@
package com.android.contacts;
-import java.util.HashMap;
import java.util.Iterator;
import java.util.ArrayList;
@@ -44,7 +43,7 @@
/**
* Collapses a list of Collapsible items into a list of collapsed items. Items are collapsed
- * if {@link Collapsible#shouldCollapseWith(Object) return strue, and are collapsed
+ * if {@link Collapsible#shouldCollapseWith(Object)} returns strue, and are collapsed
* through the {@Link Collapsible#collapseWith(Object)} function implemented by the data item.
*
* @param list ArrayList of Objects of type <T extends Collapsible<T>> to be collapsed.
diff --git a/src/com/android/contacts/ContactEntryAdapter.java b/src/com/android/contacts/ContactEntryAdapter.java
deleted file mode 100644
index 34ee505..0000000
--- a/src/com/android/contacts/ContactEntryAdapter.java
+++ /dev/null
@@ -1,262 +0,0 @@
-/*
- * 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.Context;
-import android.net.Uri;
-import android.os.Parcel;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-
-import java.util.ArrayList;
-
-public abstract class ContactEntryAdapter<E extends ContactEntryAdapter.Entry>
- extends BaseAdapter {
-
- protected ArrayList<ArrayList<E>> mSections;
- protected LayoutInflater mInflater;
- protected Context mContext;
- protected boolean mSeparators;
-
- /**
- * Base class for adapter entries.
- */
- public static class Entry {
- public int type = -1;
- public String label;
- public String data;
- public Uri uri;
- public long id = 0;
- public long contactId;
- public int maxLines = 1;
- public String mimetype;
-
- /**
- * Helper for making subclasses parcelable.
- */
- protected void writeToParcel(Parcel p) {
- p.writeInt(type);
- p.writeString(label);
- p.writeString(data);
- p.writeParcelable(uri, 0);
- p.writeLong(id);
- p.writeInt(maxLines);
- p.writeString(mimetype);
- }
-
- /**
- * Helper for making subclasses parcelable.
- */
- protected void readFromParcel(Parcel p) {
- final ClassLoader loader = getClass().getClassLoader();
- type = p.readInt();
- label = p.readString();
- data = p.readString();
- uri = p.readParcelable(loader);
- id = p.readLong();
- maxLines = p.readInt();
- mimetype = p.readString();
- }
- }
-
- ContactEntryAdapter(Context context, ArrayList<ArrayList<E>> sections, boolean separators) {
- mContext = context;
- mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- mSections = sections;
- mSeparators = separators;
- }
-
- /**
- * Resets the section data.
- *
- * @param sections the section data
- */
- public final void setSections(ArrayList<ArrayList<E>> sections, boolean separators) {
- mSections = sections;
- mSeparators = separators;
- notifyDataSetChanged();
- }
-
- /**
- * Resets the section data and returns the position of the given entry.
- *
- * @param sections the section data
- * @param entry the entry to return the position for
- * @return the position of entry, or -1 if it isn't found
- */
- public final int setSections(ArrayList<ArrayList<E>> sections, E entry) {
- mSections = sections;
- notifyDataSetChanged();
-
- int numSections = mSections.size();
- int position = 0;
- for (int i = 0; i < numSections; i++) {
- ArrayList<E> section = mSections.get(i);
- int sectionSize = section.size();
- for (int j = 0; j < sectionSize; j++) {
- E e = section.get(j);
- if (e.equals(entry)) {
- position += j;
- return position;
- }
- }
- position += sectionSize;
- }
- return -1;
- }
-
- /**
- * @see android.widget.ListAdapter#getCount()
- */
- public final int getCount() {
- return countEntries(mSections, mSeparators);
- }
-
- /**
- * @see android.widget.ListAdapter#hasSeparators()
- */
- @Override
- public final boolean areAllItemsEnabled() {
- return mSeparators == false;
- }
-
- /**
- * @see android.widget.ListAdapter#isSeparator(int)
- */
- @Override
- public final boolean isEnabled(int position) {
- if (!mSeparators) {
- return true;
- }
-
- int numSections = mSections.size();
- for (int i = 0; i < numSections; i++) {
- ArrayList<E> section = mSections.get(i);
- int sectionSize = section.size();
- if (sectionSize == 1) {
- // The section only contains a separator and nothing else, skip it
- continue;
- }
- if (position == 0) {
- // The first item in a section is always the separator
- return false;
- }
- position -= sectionSize;
- }
- return true;
- }
-
- /**
- * @see android.widget.ListAdapter#getItem(int)
- */
- public final Object getItem(int position) {
- return getEntry(mSections, position, mSeparators);
- }
-
- /**
- * Get the entry for the given position.
- *
- * @param sections the list of sections
- * @param position the position for the desired entry
- * @return the ContactEntry for the given position
- */
- public final static <T extends Entry> T getEntry(ArrayList<ArrayList<T>> sections,
- int position, boolean separators) {
- int numSections = sections.size();
- for (int i = 0; i < numSections; i++) {
- ArrayList<T> section = sections.get(i);
- int sectionSize = section.size();
- if (separators && sectionSize == 1) {
- // The section only contains a separator and nothing else, skip it
- continue;
- }
- if (position < section.size()) {
- return section.get(position);
- }
- position -= section.size();
- }
- return null;
- }
-
- /**
- * Get the count of entries in all sections
- *
- * @param sections the list of sections
- * @return the count of entries in all sections
- */
- public static <T extends Entry> int countEntries(ArrayList<ArrayList<T>> sections,
- boolean separators) {
- int count = 0;
- int numSections = sections.size();
- for (int i = 0; i < numSections; i++) {
- ArrayList<T> section = sections.get(i);
- int sectionSize = section.size();
- if (separators && sectionSize == 1) {
- // The section only contains a separator and nothing else, skip it
- continue;
- }
- count += sections.get(i).size();
- }
- return count;
- }
-
- /**
- * @see android.widget.ListAdapter#getItemId(int)
- */
- public final long getItemId(int position) {
- Entry entry = getEntry(mSections, position, mSeparators);
- if (entry != null) {
- return entry.id;
- } else {
- return -1;
- }
- }
-
- /**
- * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
- */
- public View getView(int position, View convertView, ViewGroup parent) {
- View v;
- if (convertView == null) {
- v = newView(position, parent);
- } else {
- v = convertView;
- }
- bindView(v, getEntry(mSections, position, mSeparators));
- return v;
- }
-
- /**
- * Create a new view for an entry.
- *
- * @parent the parent ViewGroup
- * @return the newly created view
- */
- protected abstract View newView(int position, ViewGroup parent);
-
- /**
- * Binds the data from an entry to a view.
- *
- * @param view the view to display the entry in
- * @param entry the data to bind
- */
- protected abstract void bindView(View view, E entry);
-}
diff --git a/src/com/android/contacts/ContactEntryListView.java b/src/com/android/contacts/ContactEntryListView.java
new file mode 100644
index 0000000..21ff25e
--- /dev/null
+++ b/src/com/android/contacts/ContactEntryListView.java
@@ -0,0 +1,87 @@
+/*
+ * 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.list.ContactEntryListAdapter;
+import com.android.contacts.widget.PinnedHeaderListView;
+import com.android.contacts.widget.TextHighlightingAnimation;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.AbsListView;
+import android.widget.ListAdapter;
+
+/**
+ * A custom list view for a list of contacts or contact-related entries. It handles
+ * animation of names on scroll.
+ */
+public class ContactEntryListView extends PinnedHeaderListView {
+
+ private static final int TEXT_HIGHLIGHTING_ANIMATION_DURATION = 350;
+
+ private final TextHighlightingAnimation mHighlightingAnimation =
+ new ContactNameHighlightingAnimation(this, TEXT_HIGHLIGHTING_ANIMATION_DURATION);
+
+ private boolean mHighlightNamesWhenScrolling;
+
+ public ContactEntryListView(Context context) {
+ this(context, null);
+ }
+
+ public ContactEntryListView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.listViewStyle);
+ }
+
+ public ContactEntryListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setPinnedHeaderBackgroundColor(
+ context.getResources().getColor(R.color.pinned_header_background));
+ }
+
+ public TextHighlightingAnimation getTextHighlightingAnimation() {
+ return mHighlightingAnimation;
+ }
+
+ public boolean getHighlightNamesWhenScrolling() {
+ return mHighlightNamesWhenScrolling;
+ }
+
+ public void setHighlightNamesWhenScrolling(boolean flag) {
+ mHighlightNamesWhenScrolling = flag;
+ }
+
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ super.setAdapter(adapter);
+ if (adapter instanceof ContactEntryListAdapter) {
+ ((ContactEntryListAdapter)adapter)
+ .setTextWithHighlightingFactory(mHighlightingAnimation);
+ }
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ super.onScrollStateChanged(view, scrollState);
+ if (mHighlightNamesWhenScrolling) {
+ if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
+ mHighlightingAnimation.startHighlighting();
+ } else {
+ mHighlightingAnimation.stopHighlighting();
+ }
+ }
+ }
+}
diff --git a/src/com/android/contacts/ContactListEmptyView.java b/src/com/android/contacts/ContactListEmptyView.java
new file mode 100644
index 0000000..40d5152
--- /dev/null
+++ b/src/com/android/contacts/ContactListEmptyView.java
@@ -0,0 +1,113 @@
+/*
+ * 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.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);
+ }
+
+ public 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/ContactNameHighlightingAnimation.java b/src/com/android/contacts/ContactNameHighlightingAnimation.java
new file mode 100644
index 0000000..68664b3
--- /dev/null
+++ b/src/com/android/contacts/ContactNameHighlightingAnimation.java
@@ -0,0 +1,60 @@
+/*
+ * 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 com.android.contacts.list.ContactListItemView;
+import com.android.contacts.widget.TextHighlightingAnimation;
+
+import android.view.View;
+import android.widget.ListView;
+
+/**
+ * A {@link TextHighlightingAnimation} that redraws just the contact display name in a
+ * list item.
+ */
+public class ContactNameHighlightingAnimation extends TextHighlightingAnimation {
+ private final ListView mListView;
+
+ public ContactNameHighlightingAnimation(ListView listView, int duration) {
+ super(duration);
+ this.mListView = listView;
+ }
+
+ /**
+ * Redraws all visible items of the list corresponding to contacts
+ */
+ @Override
+ protected void invalidate() {
+ int childCount = mListView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View itemView = mListView.getChildAt(i);
+ if (itemView instanceof ContactListItemView) {
+ final ContactListItemView view = (ContactListItemView)itemView;
+ view.getNameTextView().invalidate();
+ }
+ }
+ }
+
+ @Override
+ protected void onAnimationStarted() {
+ mListView.setScrollingCacheEnabled(false);
+ }
+
+ @Override
+ protected void onAnimationEnded() {
+ mListView.setScrollingCacheEnabled(true);
+ }
+}
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 0d2c7eb..e899d69 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -16,475 +16,94 @@
package com.android.contacts;
-import com.android.contacts.TextHighlightingAnimation.TextWithHighlighting;
+import com.android.contacts.list.CallOrSmsInitiator;
+import com.android.contacts.list.ContactBrowseListContextMenuAdapter;
+import com.android.contacts.list.ContactEntryListFragment;
+import com.android.contacts.list.ContactPickerFragment;
+import com.android.contacts.list.ContactsIntentResolver;
+import com.android.contacts.list.ContactsRequest;
+import com.android.contacts.list.DefaultContactBrowseListFragment;
+import com.android.contacts.list.OnContactBrowserActionListener;
+import com.android.contacts.list.OnContactPickerActionListener;
+import com.android.contacts.list.OnPhoneNumberPickerActionListener;
+import com.android.contacts.list.OnPostalAddressPickerActionListener;
+import com.android.contacts.list.PhoneNumberPickerFragment;
+import com.android.contacts.list.PostalAddressPickerFragment;
+import com.android.contacts.list.StrequentContactListFragment;
import com.android.contacts.model.ContactsSource;
import com.android.contacts.model.Sources;
-import com.android.contacts.ui.ContactsPreferences;
import com.android.contacts.ui.ContactsPreferencesActivity;
-import com.android.contacts.ui.ContactsPreferencesActivity.Prefs;
import com.android.contacts.util.AccountSelectionUtil;
-import com.android.contacts.util.Constants;
+import com.android.contacts.vcard.ExportVCardActivity;
+import com.android.contacts.widget.ContextMenuAdapter;
+import com.android.contacts.widget.SearchEditText;
+import com.android.contacts.widget.SearchEditText.OnFilterTextListener;
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.FragmentTransaction;
import android.app.SearchManager;
-import android.content.AsyncQueryHandler;
-import android.content.ContentResolver;
import android.content.ContentUris;
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;
-import android.content.res.ColorStateList;
import android.content.res.Resources;
-import android.database.CharArrayBuffer;
-import android.database.ContentObserver;
import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.Typeface;
-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;
-import android.provider.Contacts.ContactMethods;
-import android.provider.Contacts.People;
-import android.provider.Contacts.PeopleColumns;
-import android.provider.Contacts.Phones;
-import android.provider.ContactsContract.ContactCounts;
import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Intents;
-import android.provider.ContactsContract.ProviderStatus;
import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.SearchSnippetColumns;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-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;
-import android.text.Editable;
-import android.text.Html;
import android.text.TextUtils;
-import android.text.TextWatcher;
import android.util.Log;
-import android.view.ContextMenu;
import android.view.ContextThemeWrapper;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
-import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.View.OnClickListener;
-import android.view.View.OnFocusChangeListener;
-import android.view.View.OnTouchListener;
-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.Button;
-import android.widget.CursorAdapter;
-import android.widget.Filter;
-import android.widget.ImageView;
-import android.widget.ListView;
-import android.widget.QuickContactBadge;
-import android.widget.SectionIndexer;
import android.widget.TextView;
import android.widget.Toast;
-import android.widget.AbsListView.OnScrollListener;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
-import java.util.Random;
/**
* Displays a list of contacts. Usually is embedded into the ContactsActivity.
*/
@SuppressWarnings("deprecation")
-public class ContactsListActivity extends ListActivity implements View.OnCreateContextMenuListener,
- View.OnClickListener, View.OnKeyListener, TextWatcher, TextView.OnEditorActionListener,
- OnFocusChangeListener, OnTouchListener {
-
- public static class JoinContactActivity extends ContactsListActivity {
-
- }
-
- public static class ContactsSearchActivity extends ContactsListActivity {
-
- }
+public class ContactsListActivity extends Activity implements View.OnCreateContextMenuListener {
private static final String TAG = "ContactsListActivity";
- private static final boolean ENABLE_ACTION_ICON_OVERLAYS = true;
-
- private static final String LIST_STATE_KEY = "liststate";
- private static final String SHORTCUT_ACTION_KEY = "shortcutAction";
-
- static final int MENU_ITEM_VIEW_CONTACT = 1;
- static final int MENU_ITEM_CALL = 2;
- static final int MENU_ITEM_EDIT_BEFORE_CALL = 3;
- static final int MENU_ITEM_SEND_SMS = 4;
- static final int MENU_ITEM_SEND_IM = 5;
- static final int MENU_ITEM_EDIT = 6;
- static final int MENU_ITEM_DELETE = 7;
- static final int MENU_ITEM_TOGGLE_STAR = 8;
-
private static final int SUBACTIVITY_NEW_CONTACT = 1;
private static final int SUBACTIVITY_VIEW_CONTACT = 2;
private static final int SUBACTIVITY_DISPLAY_GROUP = 3;
private static final int SUBACTIVITY_SEARCH = 4;
- private static final int SUBACTIVITY_FILTER = 5;
- 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 =
- buildSectionIndexerUri(Contacts.CONTENT_URI);
-
- /** Mask for picker mode */
- static final int MODE_MASK_PICKER = 0x80000000;
- /** Mask for no presence mode */
- static final int MODE_MASK_NO_PRESENCE = 0x40000000;
- /** Mask for enabling list filtering */
- static final int MODE_MASK_NO_FILTER = 0x20000000;
- /** Mask for having a "create new contact" header in the list */
- static final int MODE_MASK_CREATE_NEW = 0x10000000;
- /** Mask for showing photos in the list */
- static final int MODE_MASK_SHOW_PHOTOS = 0x08000000;
- /** Mask for hiding additional information e.g. primary phone number in the list */
- static final int MODE_MASK_NO_DATA = 0x04000000;
- /** Mask for showing a call button in the list */
- static final int MODE_MASK_SHOW_CALL_BUTTON = 0x02000000;
- /** Mask to disable quickcontact (images will show as normal images) */
- static final int MODE_MASK_DISABLE_QUIKCCONTACT = 0x01000000;
- /** Mask to show the total number of contacts at the top */
- static final int MODE_MASK_SHOW_NUMBER_OF_CONTACTS = 0x00800000;
-
- /** Unknown mode */
- static final int MODE_UNKNOWN = 0;
- /** Default mode */
- static final int MODE_DEFAULT = 4 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
- /** Custom mode */
- static final int MODE_CUSTOM = 8;
- /** Show all starred contacts */
- static final int MODE_STARRED = 20 | MODE_MASK_SHOW_PHOTOS;
- /** Show frequently contacted contacts */
- static final int MODE_FREQUENT = 30 | MODE_MASK_SHOW_PHOTOS;
- /** Show starred and the frequent */
- static final int MODE_STREQUENT = 35 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_CALL_BUTTON;
- /** Show all contacts and pick them when clicking */
- static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER | MODE_MASK_SHOW_PHOTOS
- | MODE_MASK_DISABLE_QUIKCCONTACT;
- /** Show all contacts as well as the option to create a new one */
- static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW
- | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
- /** Show all people through the legacy provider and pick them when clicking */
- static final int MODE_LEGACY_PICK_PERSON = 43 | MODE_MASK_PICKER
- | MODE_MASK_DISABLE_QUIKCCONTACT;
- /** Show all people through the legacy provider as well as the option to create a new one */
- static final int MODE_LEGACY_PICK_OR_CREATE_PERSON = 44 | MODE_MASK_PICKER
- | MODE_MASK_CREATE_NEW | MODE_MASK_DISABLE_QUIKCCONTACT;
- /** Show all contacts and pick them when clicking, and allow creating a new contact */
- static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW
- | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
- /** Show all phone numbers and pick them when clicking */
- static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE;
- /** Show all phone numbers through the legacy provider and pick them when clicking */
- static final int MODE_LEGACY_PICK_PHONE =
- 51 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
- /** Show all postal addresses and pick them when clicking */
- static final int MODE_PICK_POSTAL =
- 55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
- /** Show all postal addresses and pick them when clicking */
- static final int MODE_LEGACY_PICK_POSTAL =
- 56 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
- static final int MODE_GROUP = 57 | MODE_MASK_SHOW_PHOTOS;
- /** Run a search query */
- static final int MODE_QUERY = 60 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_NO_FILTER
- | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
- /** Run a search query in PICK mode, but that still launches to VIEW */
- 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;
-
- /** Run a search query in a PICK_PHONE mode */
- static final int MODE_QUERY_PICK_PHONE = 80 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER
- | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
-
- /** Run a search query in PICK mode, but that still launches to EDIT */
- static final int MODE_QUERY_PICK_TO_EDIT = 85 | MODE_MASK_NO_FILTER | MODE_MASK_SHOW_PHOTOS
- | MODE_MASK_PICKER | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
-
- /**
- * 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;
-
- static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
- Contacts._ID, // 0
- Contacts.DISPLAY_NAME_PRIMARY, // 1
- Contacts.DISPLAY_NAME_ALTERNATIVE, // 2
- Contacts.SORT_KEY_PRIMARY, // 3
- Contacts.STARRED, // 4
- Contacts.TIMES_CONTACTED, // 5
- Contacts.CONTACT_PRESENCE, // 6
- Contacts.PHOTO_ID, // 7
- Contacts.LOOKUP_KEY, // 8
- Contacts.PHONETIC_NAME, // 9
- Contacts.HAS_PHONE_NUMBER, // 10
- };
- static final String[] CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL = new String[] {
- Contacts._ID, // 0
- Contacts.DISPLAY_NAME_PRIMARY, // 1
- Contacts.DISPLAY_NAME_ALTERNATIVE, // 2
- Contacts.SORT_KEY_PRIMARY, // 3
- Contacts.STARRED, // 4
- Contacts.TIMES_CONTACTED, // 5
- Contacts.CONTACT_PRESENCE, // 6
- Contacts.PHOTO_ID, // 7
- Contacts.LOOKUP_KEY, // 8
- Contacts.PHONETIC_NAME, // 9
- // email lookup doesn't included HAS_PHONE_NUMBER in projection
- };
-
- static final String[] CONTACTS_SUMMARY_FILTER_PROJECTION = new String[] {
- Contacts._ID, // 0
- Contacts.DISPLAY_NAME_PRIMARY, // 1
- Contacts.DISPLAY_NAME_ALTERNATIVE, // 2
- Contacts.SORT_KEY_PRIMARY, // 3
- Contacts.STARRED, // 4
- Contacts.TIMES_CONTACTED, // 5
- Contacts.CONTACT_PRESENCE, // 6
- Contacts.PHOTO_ID, // 7
- Contacts.LOOKUP_KEY, // 8
- Contacts.PHONETIC_NAME, // 9
- Contacts.HAS_PHONE_NUMBER, // 10
- SearchSnippetColumns.SNIPPET_MIMETYPE, // 11
- SearchSnippetColumns.SNIPPET_DATA1, // 12
- SearchSnippetColumns.SNIPPET_DATA4, // 13
- };
-
- static final String[] LEGACY_PEOPLE_PROJECTION = new String[] {
- People._ID, // 0
- People.DISPLAY_NAME, // 1
- People.DISPLAY_NAME, // 2
- People.DISPLAY_NAME, // 3
- People.STARRED, // 4
- PeopleColumns.TIMES_CONTACTED, // 5
- People.PRESENCE_STATUS, // 6
- };
- static final int SUMMARY_ID_COLUMN_INDEX = 0;
- static final int SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1;
- static final int SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2;
- static final int SUMMARY_SORT_KEY_PRIMARY_COLUMN_INDEX = 3;
- static final int SUMMARY_STARRED_COLUMN_INDEX = 4;
- static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 5;
- static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 6;
- static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 7;
- static final int SUMMARY_LOOKUP_KEY_COLUMN_INDEX = 8;
- static final int SUMMARY_PHONETIC_NAME_COLUMN_INDEX = 9;
- static final int SUMMARY_HAS_PHONE_COLUMN_INDEX = 10;
- static final int SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX = 11;
- static final int SUMMARY_SNIPPET_DATA1_COLUMN_INDEX = 12;
- static final int SUMMARY_SNIPPET_DATA4_COLUMN_INDEX = 13;
-
- static final String[] PHONES_PROJECTION = new String[] {
- Phone._ID, //0
- Phone.TYPE, //1
- Phone.LABEL, //2
- Phone.NUMBER, //3
- Phone.DISPLAY_NAME, // 4
- Phone.CONTACT_ID, // 5
- };
- static final String[] LEGACY_PHONES_PROJECTION = new String[] {
- Phones._ID, //0
- Phones.TYPE, //1
- Phones.LABEL, //2
- Phones.NUMBER, //3
- People.DISPLAY_NAME, // 4
- };
- static final int PHONE_ID_COLUMN_INDEX = 0;
- static final int PHONE_TYPE_COLUMN_INDEX = 1;
- static final int PHONE_LABEL_COLUMN_INDEX = 2;
- 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 String[] POSTALS_PROJECTION = new String[] {
- StructuredPostal._ID, //0
- StructuredPostal.TYPE, //1
- StructuredPostal.LABEL, //2
- StructuredPostal.DATA, //3
- StructuredPostal.DISPLAY_NAME, // 4
- };
- static final String[] LEGACY_POSTALS_PROJECTION = new String[] {
- ContactMethods._ID, //0
- ContactMethods.TYPE, //1
- ContactMethods.LABEL, //2
- ContactMethods.DATA, //3
- People.DISPLAY_NAME, // 4
- };
- static final String[] RAW_CONTACTS_PROJECTION = new String[] {
+ private static final String[] RAW_CONTACTS_PROJECTION = new String[] {
RawContacts._ID, //0
RawContacts.CONTACT_ID, //1
RawContacts.ACCOUNT_TYPE, //2
};
- static final int POSTAL_ID_COLUMN_INDEX = 0;
- static final int POSTAL_TYPE_COLUMN_INDEX = 1;
- static final int POSTAL_LABEL_COLUMN_INDEX = 2;
- static final int POSTAL_ADDRESS_COLUMN_INDEX = 3;
- static final int POSTAL_DISPLAY_NAME_COLUMN_INDEX = 4;
-
- private static final int QUERY_TOKEN = 42;
-
- static final String KEY_PICKER_MODE = "picker_mode";
-
- private ContactItemListAdapter mAdapter;
-
- int mMode = MODE_DEFAULT;
-
- private QueryHandler mQueryHandler;
- private boolean mJustCreated;
- private boolean mSyncEnabled;
- Uri mSelectedContactUri;
-
-// private boolean mDisplayAll;
- private boolean mDisplayOnlyPhones;
-
- private Uri mGroupUri;
-
- private long mQueryAggregateId;
+ private Uri mSelectedContactUri;
private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
private int mWritableSourcesCnt;
private int mReadOnlySourcesCnt;
- /**
- * Used to keep track of the scroll state of the list.
- */
- private Parcelable mListState = null;
-
- private String mShortcutAction;
-
- /**
- * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
- */
- private int mQueryMode = QUERY_MODE_NONE;
-
- private static final int QUERY_MODE_NONE = -1;
- private static final int QUERY_MODE_MAILTO = 1;
- private static final int QUERY_MODE_TEL = 2;
-
- private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
-
- private boolean mSearchMode;
- private boolean mSearchResultsMode;
- private boolean mShowNumberOfContacts;
-
- private boolean mShowSearchSnippets;
- private boolean mSearchInitiated;
-
- private String mInitialFilter;
-
- 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;
- private static final UriMatcher sContactsIdMatcher;
-
- private ContactPhotoLoader mPhotoLoader;
-
- final String[] sLookupProjection = new String[] {
+ private final String[] sLookupProjection = new String[] {
Contacts.LOOKUP_KEY
};
-
- static {
- sContactsIdMatcher = new UriMatcher(UriMatcher.NO_MATCH);
- sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
- }
-
private class DeleteClickListener implements DialogInterface.OnClickListener {
public void onClick(DialogInterface dialog, int which) {
if (mSelectedContactUri != null) {
@@ -493,747 +112,372 @@
}
}
- /**
- * A {@link TextHighlightingAnimation} that redraws just the contact display name in a
- * list item.
- */
- private static class NameHighlightingAnimation extends TextHighlightingAnimation {
- private final ListView mListView;
+ private ContactsIntentResolver mIntentResolver;
+ protected ContactEntryListFragment<?> mListFragment;
- private NameHighlightingAnimation(ListView listView, int duration) {
- super(duration);
- this.mListView = listView;
- }
+ protected CallOrSmsInitiator mCallOrSmsInitiator;
- /**
- * Redraws all visible items of the list corresponding to contacts
- */
- @Override
- protected void invalidate() {
- int childCount = mListView.getChildCount();
- for (int i = 0; i < childCount; i++) {
- View itemView = mListView.getChildAt(i);
- if (itemView instanceof ContactListItemView) {
- final ContactListItemView view = (ContactListItemView)itemView;
- view.getNameTextView().invalidate();
- }
- }
- }
+ private int mActionCode;
- @Override
- protected void onAnimationStarted() {
- mListView.setScrollingCacheEnabled(false);
- }
+ private boolean mSearchInitiated;
- @Override
- protected void onAnimationEnded() {
- mListView.setScrollingCacheEnabled(true);
- }
- }
-
- // The size of a home screen shortcut icon.
- private int mIconSize;
- private ContactsPreferences mContactsPrefs;
- private int mDisplayOrder;
- private int mSortOrder;
- private boolean mHighlightWhenScrolling;
- private TextHighlightingAnimation mHighlightingAnimation;
+ private ContactsRequest mRequest;
private SearchEditText mSearchEditText;
+ public ContactsListActivity() {
+ mIntentResolver = new ContactsIntentResolver(this);
+ }
+
/**
- * An approximation of the background color of the pinned header. This color
- * is used when the pinned header is being pushed up. At that point the header
- * "fades away". Rather than computing a faded bitmap based on the 9-patch
- * normally used for the background, we will use a solid color, which will
- * provide better performance and reduced complexity.
+ * Visible for testing: makes queries run on the UI thread.
*/
- private int mPinnedHeaderBackgroundColor;
-
- private ContentObserver mProviderStatusObserver = new ContentObserver(new Handler()) {
-
- @Override
- public void onChange(boolean selfChange) {
- checkProviderState(true);
- }
- };
+ /* package */ void runQueriesSynchronously() {
+ // TODO
+ }
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
- mIconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
- mContactsPrefs = new ContactsPreferences(this);
- mPhotoLoader = new ContactPhotoLoader(this, R.drawable.ic_contact_list_picture);
-
- // Resolve the intent
- final Intent intent = getIntent();
-
- // 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) {
- setTitle(title);
- }
-
- String action = intent.getAction();
- String component = intent.getComponent().getClassName();
-
- // 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
- // context for the search queries.
- if (UI.FILTER_CONTACTS_ACTION.equals(action)) {
- mSearchMode = true;
- mShowSearchSnippets = true;
- Bundle extras = intent.getExtras();
- if (extras != null) {
- mInitialFilter = extras.getString(UI.FILTER_TEXT_EXTRA_KEY);
- String originalAction =
- extras.getString(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY);
- if (originalAction != null) {
- action = originalAction;
- }
- String originalComponent =
- extras.getString(ContactsSearchManager.ORIGINAL_COMPONENT_EXTRA_KEY);
- if (originalComponent != null) {
- component = originalComponent;
- }
- } else {
- mInitialFilter = null;
- }
- }
-
- Log.i(TAG, "Called with action: " + action);
- mMode = MODE_UNKNOWN;
- if (UI.LIST_DEFAULT.equals(action) || UI.FILTER_CONTACTS_ACTION.equals(action)) {
- mMode = MODE_DEFAULT;
- // When mDefaultMode is true the mode is set in onResume(), since the preferneces
- // activity may change it whenever this activity isn't running
- } else if (UI.LIST_GROUP_ACTION.equals(action)) {
- mMode = MODE_GROUP;
- String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY);
- if (TextUtils.isEmpty(groupName)) {
- finish();
- return;
- }
- buildUserGroupUri(groupName);
- } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
- mMode = MODE_CUSTOM;
- mDisplayOnlyPhones = false;
- } else if (UI.LIST_STARRED_ACTION.equals(action)) {
- mMode = mSearchMode ? MODE_DEFAULT : MODE_STARRED;
- } else if (UI.LIST_FREQUENT_ACTION.equals(action)) {
- mMode = mSearchMode ? MODE_DEFAULT : MODE_FREQUENT;
- } else if (UI.LIST_STREQUENT_ACTION.equals(action)) {
- mMode = mSearchMode ? MODE_DEFAULT : MODE_STREQUENT;
- } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
- mMode = MODE_CUSTOM;
- mDisplayOnlyPhones = true;
- } 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)) {
- mMode = MODE_PICK_CONTACT;
- } else if (People.CONTENT_TYPE.equals(type)) {
- mMode = MODE_LEGACY_PICK_PERSON;
- } else if (Phone.CONTENT_TYPE.equals(type)) {
- mMode = MODE_PICK_PHONE;
- } else if (Phones.CONTENT_TYPE.equals(type)) {
- mMode = MODE_LEGACY_PICK_PHONE;
- } else if (StructuredPostal.CONTENT_TYPE.equals(type)) {
- mMode = MODE_PICK_POSTAL;
- } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) {
- mMode = MODE_LEGACY_PICK_POSTAL;
- }
- } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
- if (component.equals("alias.DialShortcut")) {
- mMode = MODE_PICK_PHONE;
- mShortcutAction = Intent.ACTION_CALL;
- mShowSearchSnippets = false;
- setTitle(R.string.callShortcutActivityTitle);
- } else if (component.equals("alias.MessageShortcut")) {
- mMode = MODE_PICK_PHONE;
- mShortcutAction = Intent.ACTION_SENDTO;
- mShowSearchSnippets = false;
- setTitle(R.string.messageShortcutActivityTitle);
- } else if (mSearchMode) {
- mMode = MODE_PICK_CONTACT;
- mShortcutAction = Intent.ACTION_VIEW;
- setTitle(R.string.shortcutActivityTitle);
- } else {
- mMode = MODE_PICK_OR_CREATE_CONTACT;
- mShortcutAction = Intent.ACTION_VIEW;
- 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)) {
- if (mSearchMode) {
- mMode = MODE_PICK_CONTACT;
- } else {
- mMode = MODE_PICK_OR_CREATE_CONTACT;
- }
- } else if (Phone.CONTENT_ITEM_TYPE.equals(type)) {
- mMode = MODE_PICK_PHONE;
- } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
- mMode = MODE_LEGACY_PICK_PHONE;
- } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(type)) {
- mMode = MODE_PICK_POSTAL;
- } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
- mMode = MODE_LEGACY_PICK_POSTAL;
- } else if (People.CONTENT_ITEM_TYPE.equals(type)) {
- if (mSearchMode) {
- mMode = MODE_LEGACY_PICK_PERSON;
- } else {
- mMode = MODE_LEGACY_PICK_OR_CREATE_PERSON;
- }
- }
-
- } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) {
- mMode = MODE_INSERT_OR_EDIT_CONTACT;
- } else if (Intent.ACTION_SEARCH.equals(action)) {
- // See if the suggestion was clicked with a search action key (call button)
- if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
- String query = intent.getStringExtra(SearchManager.QUERY);
- if (!TextUtils.isEmpty(query)) {
- Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
- Uri.fromParts("tel", query, null));
- startActivity(newIntent);
- }
- finish();
- return;
- }
-
- // See if search request has extras to specify query
- if (intent.hasExtra(Insert.EMAIL)) {
- mMode = MODE_QUERY_PICK_TO_VIEW;
- mQueryMode = QUERY_MODE_MAILTO;
- mInitialFilter = intent.getStringExtra(Insert.EMAIL);
- } else if (intent.hasExtra(Insert.PHONE)) {
- mMode = MODE_QUERY_PICK_TO_VIEW;
- mQueryMode = QUERY_MODE_TEL;
- mInitialFilter = intent.getStringExtra(Insert.PHONE);
- } else {
- // Otherwise handle the more normal search case
- mMode = MODE_QUERY;
- mShowSearchSnippets = true;
- mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY);
- }
- mSearchResultsMode = true;
- } else if (ACTION_SEARCH_INTERNAL.equals(action)) {
- String originalAction = null;
- Bundle extras = intent.getExtras();
- if (extras != null) {
- originalAction = extras.getString(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY);
- }
- mShortcutAction = intent.getStringExtra(SHORTCUT_ACTION_KEY);
-
- if (Intent.ACTION_INSERT_OR_EDIT.equals(originalAction)) {
- mMode = MODE_QUERY_PICK_TO_EDIT;
- mShowSearchSnippets = true;
- mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY);
- } else if (mShortcutAction != null && intent.hasExtra(Insert.PHONE)) {
- mMode = MODE_QUERY_PICK_PHONE;
- mQueryMode = QUERY_MODE_TEL;
- mInitialFilter = intent.getStringExtra(Insert.PHONE);
- } else {
- mMode = MODE_QUERY_PICK;
- mQueryMode = QUERY_MODE_NONE;
- mShowSearchSnippets = true;
- mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY);
- }
- mSearchResultsMode = true;
- // Since this is the filter activity it receives all intents
- // dispatched from the SearchManager for security reasons
- // so we need to re-dispatch from here to the intended target.
- } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) {
- Uri data = intent.getData();
- Uri telUri = null;
- if (sContactsIdMatcher.match(data) == CONTACTS_ID) {
- long contactId = Long.valueOf(data.getLastPathSegment());
- final Cursor cursor = queryPhoneNumbers(contactId);
- if (cursor != null) {
- if (cursor.getCount() == 1 && cursor.moveToFirst()) {
- int phoneNumberIndex = cursor.getColumnIndex(Phone.NUMBER);
- String phoneNumber = cursor.getString(phoneNumberIndex);
- telUri = Uri.parse("tel:" + phoneNumber);
- }
- cursor.close();
- }
- }
- // See if the suggestion was clicked with a search action key (call button)
- Intent newIntent;
- if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG)) && telUri != null) {
- newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, telUri);
- } else {
- newIntent = new Intent(Intent.ACTION_VIEW, data);
- }
- startActivity(newIntent);
- finish();
- return;
- } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) {
- Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
- startActivity(newIntent);
- finish();
- return;
- } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) {
- // TODO actually support this in EditContactActivity.
- String number = intent.getData().getSchemeSpecificPart();
- Intent newIntent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
- newIntent.putExtra(Intents.Insert.PHONE, number);
- startActivity(newIntent);
+ // Extract relevant information from the intent
+ mRequest = mIntentResolver.resolveIntent(getIntent());
+ if (!mRequest.isValid()) {
+ setResult(RESULT_CANCELED);
finish();
return;
}
- if (JOIN_AGGREGATE.equals(action)) {
- if (mSearchMode) {
- mMode = MODE_PICK_CONTACT;
- } 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();
- }
- }
+ Intent redirect = mRequest.getRedirectIntent();
+ if (redirect != null) {
+ // Need to start a different activity
+ startActivity(redirect);
+ finish();
+ return;
}
- if (mMode == MODE_UNKNOWN) {
- mMode = MODE_DEFAULT;
- }
+ setTitle(mRequest.getActivityTitle());
- if (((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0 || mSearchMode)
- && !mSearchResultsMode) {
- mShowNumberOfContacts = true;
- }
+ onCreateFragment();
- 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) {
+ int listFragmentContainerId;
+ if (mRequest.isSearchMode()) {
setContentView(R.layout.contacts_search_content);
- } else if (mSearchResultsMode) {
- setContentView(R.layout.contacts_list_search_results);
- TextView titleText = (TextView)findViewById(R.id.search_results_for);
- titleText.setText(Html.fromHtml(getString(R.string.search_results_for,
- "<b>" + mInitialFilter + "</b>")));
+ listFragmentContainerId = R.id.list_container;
+ setupSearchUI();
} else {
- setContentView(R.layout.contacts_list_content);
+ listFragmentContainerId = android.R.id.content;
}
-
- setupListView();
- if (mSearchMode) {
- setupSearchView();
- }
-
- mQueryHandler = new QueryHandler(this);
- mJustCreated = true;
-
- mSyncEnabled = true;
+ FragmentTransaction transaction = openFragmentTransaction();
+ transaction.add(listFragmentContainerId, mListFragment);
+ transaction.commit();
}
- /**
- * Register an observer for provider status changes - we will need to
- * reflect them in the UI.
- */
- private void registerProviderStatusObserver() {
- getContentResolver().registerContentObserver(ProviderStatus.CONTENT_URI,
- false, mProviderStatusObserver);
- }
-
- /**
- * Register an observer for provider status changes - we will need to
- * reflect them in the UI.
- */
- private void unregisterProviderStatusObserver() {
- getContentResolver().unregisterContentObserver(mProviderStatusObserver);
- }
-
- private void setupListView() {
- final ListView list = getListView();
- final LayoutInflater inflater = getLayoutInflater();
-
- mHighlightingAnimation =
- new NameHighlightingAnimation(list, TEXT_HIGHLIGHTING_ANIMATION_DURATION);
-
- // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
- // them when an A-Z headers is visible.
- list.setDividerHeight(0);
- list.setOnCreateContextMenuListener(this);
-
- mAdapter = new ContactItemListAdapter(this);
- setListAdapter(mAdapter);
-
- if (list instanceof PinnedHeaderListView && mAdapter.getDisplaySectionHeadersEnabled()) {
- mPinnedHeaderBackgroundColor =
- getResources().getColor(R.color.pinned_header_background);
- PinnedHeaderListView pinnedHeaderList = (PinnedHeaderListView)list;
- View pinnedHeader = inflater.inflate(R.layout.list_section, list, false);
- pinnedHeaderList.setPinnedHeaderView(pinnedHeader);
- }
-
- list.setOnScrollListener(mAdapter);
- list.setOnKeyListener(this);
- list.setOnFocusChangeListener(this);
- list.setOnTouchListener(this);
-
- // We manually save/restore the listview state
- list.setSaveEnabled(false);
- }
-
- /**
- * Configures search UI.
- */
- private void setupSearchView() {
+ private void setupSearchUI() {
mSearchEditText = (SearchEditText)findViewById(R.id.search_src_text);
- mSearchEditText.addTextChangedListener(this);
- 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);
+ mSearchEditText.setText(mRequest.getQueryString());
+ mSearchEditText.setOnFilterTextListener(new OnFilterTextListener() {
+ public void onFilterChange(String queryString) {
+ mListFragment.setQueryString(queryString);
}
- } finally {
- if (c != null) {
- c.close();
+
+ public void onCancelSearch() {
+ finish();
}
- }
-
- if (contactName == null) {
- contactName = "";
- }
-
- return contactName;
- }
-
- private int getSummaryDisplayNameColumnIndex() {
- if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
- return SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
- } else {
- return SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
- }
- }
-
- /** {@inheritDoc} */
- public void onClick(View v) {
- int id = v.getId();
- switch (id) {
- // TODO a better way of identifying the button
- case android.R.id.button1: {
- final int position = (Integer)v.getTag();
- Cursor c = mAdapter.getCursor();
- if (c != null) {
- c.moveToPosition(position);
- callContact(c);
- }
- 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);
- }
-
- /**
- * Sets the mode when the request is for "default"
- */
- private void setDefaultMode() {
- // Load the preferences
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
-
- mDisplayOnlyPhones = prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
- Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mPhotoLoader.stop();
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- unregisterProviderStatusObserver();
+ });
}
@Override
protected void onResume() {
super.onResume();
-
- registerProviderStatusObserver();
- mPhotoLoader.resume();
-
- Activity parent = getParent();
-
- // Do this before setting the filter. The filter thread relies
- // on some state that is initialized in setDefaultMode
- if (mMode == MODE_DEFAULT) {
- // If we're in default mode we need to possibly reset the mode due to a change
- // in the preferences activity while we weren't running
- setDefaultMode();
- }
-
- // See if we were invoked with a filter
- if (mSearchMode) {
+ if (mRequest.isSearchMode()) {
mSearchEditText.requestFocus();
}
-
- if (!mSearchMode && !checkProviderState(mJustCreated)) {
- return;
- }
-
- if (mJustCreated) {
- // We need to start a query here the first time the activity is launched, as long
- // as we aren't doing a filter.
- startQuery();
- }
- mJustCreated = false;
- mSearchInitiated = false;
}
/**
- * Obtains the contacts provider status and configures the UI accordingly.
- *
- * @param loadData true if the method needs to start a query when the
- * provider is in the normal state
- * @return true if the provider status is normal
+ * Creates the fragment based on the current request.
*/
- private boolean checkProviderState(boolean loadData) {
- View importFailureView = findViewById(R.id.import_failure);
- if (importFailureView == null) {
- return true;
- }
+ private void onCreateFragment() {
+ mActionCode = mRequest.getActionCode();
+ switch (mActionCode) {
+ case ContactsRequest.ACTION_DEFAULT:
+ case ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT: {
+ DefaultContactBrowseListFragment fragment = new DefaultContactBrowseListFragment();
+ fragment.setOnContactListActionListener(new ContactBrowserActionListener());
- TextView messageView = (TextView) findViewById(R.id.emptyText);
-
- // 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;
-
- 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_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;
- }
+ if (mActionCode == ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT) {
+ fragment.setEditMode(true);
+ fragment.setCreateContactEnabled(true);
}
+
+ fragment.setDisplayWithPhonesOnlyOption(mRequest.getDisplayWithPhonesOnlyOption());
+
+ fragment.setVisibleContactsRestrictionEnabled(
+ !mRequest.isSearchResultsMode()
+ && mRequest.getDisplayOnlyVisible());
+
+ fragment.setContextMenuAdapter(new ContactBrowseListContextMenuAdapter(fragment));
+ fragment.setSearchMode(mRequest.isSearchMode());
+ fragment.setSearchResultsMode(mRequest.isSearchResultsMode());
+ fragment.setQueryString(mRequest.getQueryString());
+ mListFragment = fragment;
+ break;
}
- } finally {
- cursor.close();
- }
- importFailureView.setVisibility(
- mProviderStatus == ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY
- ? View.VISIBLE
- : View.GONE);
- return mProviderStatus == ProviderStatus.STATUS_NORMAL;
+ case ContactsRequest.ACTION_GROUP: {
+ throw new UnsupportedOperationException("Not yet implemented");
+ }
+
+ case ContactsRequest.ACTION_STARRED: {
+ StrequentContactListFragment fragment = new StrequentContactListFragment();
+ fragment.setOnContactListActionListener(new ContactBrowserActionListener());
+ fragment.setFrequentlyContactedContactsIncluded(false);
+ fragment.setStarredContactsIncluded(true);
+ mListFragment = fragment;
+ break;
+ }
+
+ case ContactsRequest.ACTION_FREQUENT: {
+ StrequentContactListFragment fragment = new StrequentContactListFragment();
+ fragment.setOnContactListActionListener(new ContactBrowserActionListener());
+ fragment.setFrequentlyContactedContactsIncluded(true);
+ fragment.setStarredContactsIncluded(false);
+ mListFragment = fragment;
+ break;
+ }
+
+ case ContactsRequest.ACTION_STREQUENT: {
+ StrequentContactListFragment fragment = new StrequentContactListFragment();
+ fragment.setOnContactListActionListener(new ContactBrowserActionListener());
+ fragment.setFrequentlyContactedContactsIncluded(true);
+ fragment.setStarredContactsIncluded(true);
+ mListFragment = fragment;
+ break;
+ }
+
+ case ContactsRequest.ACTION_PICK_CONTACT: {
+ ContactPickerFragment fragment = new ContactPickerFragment();
+ fragment.setOnContactPickerActionListener(new ContactPickerActionListener());
+ fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+ fragment.setSearchMode(mRequest.isSearchMode());
+ mListFragment = fragment;
+ break;
+ }
+
+ case ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT: {
+ ContactPickerFragment fragment = new ContactPickerFragment();
+ fragment.setOnContactPickerActionListener(new ContactPickerActionListener());
+ fragment.setCreateContactEnabled(!mRequest.isSearchMode());
+ fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+ mListFragment = fragment;
+ break;
+ }
+
+ case ContactsRequest.ACTION_CREATE_SHORTCUT_CONTACT: {
+ ContactPickerFragment fragment = new ContactPickerFragment();
+ fragment.setOnContactPickerActionListener(new ContactPickerActionListener());
+ fragment.setCreateContactEnabled(!mRequest.isSearchMode());
+ fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+ fragment.setSearchMode(mRequest.isSearchMode());
+ fragment.setQueryString(mRequest.getQueryString());
+ fragment.setShortcutRequested(true);
+ mListFragment = fragment;
+ break;
+ }
+
+ case ContactsRequest.ACTION_PICK_PHONE: {
+ PhoneNumberPickerFragment fragment = new PhoneNumberPickerFragment();
+ fragment.setOnPhoneNumberPickerActionListener(
+ new PhoneNumberPickerActionListener());
+ fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+ mListFragment = fragment;
+ break;
+ }
+
+ case ContactsRequest.ACTION_CREATE_SHORTCUT_CALL: {
+ PhoneNumberPickerFragment fragment = new PhoneNumberPickerFragment();
+ fragment.setOnPhoneNumberPickerActionListener(
+ new PhoneNumberPickerActionListener());
+ fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+ fragment.setShortcutAction(Intent.ACTION_CALL);
+ fragment.setSearchMode(mRequest.isSearchMode());
+
+ mListFragment = fragment;
+ break;
+ }
+
+ case ContactsRequest.ACTION_CREATE_SHORTCUT_SMS: {
+ PhoneNumberPickerFragment fragment = new PhoneNumberPickerFragment();
+ fragment.setOnPhoneNumberPickerActionListener(
+ new PhoneNumberPickerActionListener());
+ fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+ fragment.setShortcutAction(Intent.ACTION_SENDTO);
+ fragment.setSearchMode(mRequest.isSearchMode());
+
+ mListFragment = fragment;
+ break;
+ }
+
+ case ContactsRequest.ACTION_PICK_POSTAL: {
+ PostalAddressPickerFragment fragment = new PostalAddressPickerFragment();
+ fragment.setOnPostalAddressPickerActionListener(
+ new PostalAddressPickerActionListener());
+ fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+ mListFragment = fragment;
+ break;
+ }
+
+ default:
+ throw new IllegalStateException("Invalid action code: " + mActionCode);
+ }
+ mListFragment.setContactsRequest(mRequest);
}
- private void configureImportFailureView(View importFailureView) {
-
- OnClickListener listener = new OnClickListener(){
-
- public void onClick(View v) {
- switch(v.getId()) {
- case R.id.import_failure_uninstall_apps: {
- startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
- break;
- }
- case R.id.import_failure_retry_upgrade: {
- // Send a provider status update, which will trigger a retry
- ContentValues values = new ContentValues();
- values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING);
- getContentResolver().update(ProviderStatus.CONTENT_URI, values, null, null);
- break;
- }
- }
- }};
-
- Button uninstallApps = (Button) findViewById(R.id.import_failure_uninstall_apps);
- uninstallApps.setOnClickListener(listener);
-
- Button retryUpgrade = (Button) findViewById(R.id.import_failure_retry_upgrade);
- retryUpgrade.setOnClickListener(listener);
- }
-
- private String getTextFilter() {
- if (mSearchEditText != null) {
- return mSearchEditText.getText().toString();
- }
- return null;
- }
-
- @Override
- protected void onRestart() {
- super.onRestart();
-
- if (!checkProviderState(false)) {
- return;
+ private final class ContactBrowserActionListener implements OnContactBrowserActionListener {
+ public void onSearchAllContactsAction(String queryString) {
+ searchAllContacts(queryString, false);
}
- // The cursor was killed off in onStop(), so we need to get a new one here
- // We do not perform the query if a filter is set on the list because the
- // filter will cause the query to happen anyway
- if (TextUtils.isEmpty(getTextFilter())) {
- startQuery();
- } else {
- // Run the filtered query on the adapter
- mAdapter.onContentChanged();
+ public void onViewContactAction(Uri contactLookupUri) {
+ startActivity(new Intent(Intent.ACTION_VIEW, contactLookupUri));
+ }
+
+ public void onCreateNewContactAction() {
+ Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
+ Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ startActivity(intent);
+ }
+
+ public void onEditContactAction(Uri contactLookupUri) {
+ Intent intent = new Intent(Intent.ACTION_EDIT, contactLookupUri);
+ Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ startActivity(intent);
+ }
+
+ public void onAddToFavoritesAction(Uri contactUri) {
+ ContentValues values = new ContentValues(1);
+ values.put(Contacts.STARRED, 1);
+ getContentResolver().update(contactUri, values, null, null);
+ }
+
+ public void onRemoveFromFavoritesAction(Uri contactUri) {
+ ContentValues values = new ContentValues(1);
+ values.put(Contacts.STARRED, 0);
+ getContentResolver().update(contactUri, values, null, null);
+ }
+
+ public void onCallContactAction(Uri contactUri) {
+ getCallOrSmsInitiator().initiateCall(contactUri);
+ }
+
+ public void onSmsContactAction(Uri contactUri) {
+ getCallOrSmsInitiator().initiateSms(contactUri);
+ }
+
+ public void onDeleteContactAction(Uri contactUri) {
+ doContactDelete(contactUri);
+ }
+
+ public void onFinishAction() {
+ onBackPressed();
}
}
- @Override
- protected void onSaveInstanceState(Bundle icicle) {
- super.onSaveInstanceState(icicle);
- // 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());
+ private final class ContactPickerActionListener implements OnContactPickerActionListener {
+ public void onSearchAllContactsAction(String queryString) {
+ searchAllContacts(queryString, true);
+ }
+
+ public void onCreateNewContactAction() {
+ Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
+ startActivityAndForwardResult(intent);
+ }
+
+ public void onPickContactAction(Uri contactUri) {
+ Intent intent = new Intent();
+ setResult(RESULT_OK, intent.setData(contactUri));
+ finish();
+ }
+
+ public void onShortcutIntentCreated(Intent intent) {
+ setResult(RESULT_OK, intent);
+ finish();
}
}
- @Override
- protected void onRestoreInstanceState(Bundle icicle) {
- super.onRestoreInstanceState(icicle);
- // Retrieve list state. This will be applied after the QueryHandler has run
- mListState = icicle.getParcelable(LIST_STATE_KEY);
+ private final class PhoneNumberPickerActionListener implements
+ OnPhoneNumberPickerActionListener {
+ public void onSearchAllContactsAction(String queryString) {
+ searchAllContacts(queryString, true);
+ }
+
+ public void onPickPhoneNumberAction(Uri dataUri) {
+ Intent intent = new Intent();
+ setResult(RESULT_OK, intent.setData(dataUri));
+ finish();
+ }
+
+ public void onShortcutIntentCreated(Intent intent) {
+ setResult(RESULT_OK, intent);
+ finish();
+ }
}
- @Override
- protected void onStop() {
- super.onStop();
-
- mAdapter.setSuggestionsCursor(null);
- mAdapter.changeCursor(null);
-
- if (mMode == MODE_QUERY) {
- // Make sure the search box is closed
- SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
- searchManager.stopSearch();
+ private final class PostalAddressPickerActionListener implements
+ OnPostalAddressPickerActionListener {
+ public void onSearchAllContactsAction(String queryString) {
+ searchAllContacts(queryString, true);
}
+
+ public void onPickPostalAddressAction(Uri dataUri) {
+ Intent intent = new Intent();
+ setResult(RESULT_OK, intent.setData(dataUri));
+ finish();
+ }
+ }
+
+ public void startActivityAndForwardResult(final Intent intent) {
+ intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+
+ // Forward extras to the new activity
+ Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ startActivity(intent);
+ finish();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
- // 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) {
+ MenuInflater inflater = getMenuInflater();
+ if (mActionCode == ContactsRequest.ACTION_DEFAULT ||
+ mActionCode == ContactsRequest.ACTION_STREQUENT) {
+ inflater.inflate(R.menu.list, menu);
+ return true;
+ } else if (!mListFragment.isSearchMode()) {
+ inflater.inflate(R.menu.search, menu);
+ return true;
+ } else {
return false;
}
-
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.list, menu);
- return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
- final boolean defaultMode = (mMode == MODE_DEFAULT);
- menu.findItem(R.id.menu_display_groups).setVisible(defaultMode);
+ MenuItem displayGroups = menu.findItem(R.id.menu_display_groups);
+ if (displayGroups != null) {
+ displayGroups.setVisible(
+ mActionCode == ContactsRequest.ACTION_DEFAULT);
+ }
return true;
}
@@ -1260,7 +504,7 @@
}
case R.id.menu_accounts: {
final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
- intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
+ intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[] {
ContactsContract.AUTHORITY
});
startActivity(intent);
@@ -1273,74 +517,35 @@
@Override
public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
boolean globalSearch) {
- if (mProviderStatus != ProviderStatus.STATUS_NORMAL) {
- return;
- }
+// TODO
+// if (mProviderStatus != ProviderStatus.STATUS_NORMAL) {
+// return;
+// }
if (globalSearch) {
super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
} else {
- if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0) {
- if ((mMode & MODE_MASK_PICKER) != 0) {
- ContactsSearchManager.startSearchForResult(this, initialQuery,
- SUBACTIVITY_FILTER);
- } else {
- ContactsSearchManager.startSearch(this, initialQuery);
- }
- }
+ mListFragment.startSearch(initialQuery);
}
}
/**
- * Performs filtering of the list based on the search query entered in the
- * search text edit.
- */
- protected void onSearchTextChanged() {
- // Set the proper empty string
- setEmptyText();
-
- Filter filter = mAdapter.getFilter();
- filter.filter(getTextFilter());
- }
-
- /**
* Starts a new activity that will run a search query and display search results.
*/
- private void doSearch() {
- String query = getTextFilter();
+ protected void searchAllContacts(String queryString, boolean returnResult) {
+ String query = mListFragment.getQueryString();
if (TextUtils.isEmpty(query)) {
return;
}
Intent intent = new Intent(this, SearchResultsActivity.class);
- Intent originalIntent = getIntent();
- Bundle originalExtras = originalIntent.getExtras();
- if (originalExtras != null) {
- intent.putExtras(originalExtras);
- }
-
+ intent.setAction(Intent.ACTION_SEARCH);
intent.putExtra(SearchManager.QUERY, query);
- if ((mMode & MODE_MASK_PICKER) != 0) {
- intent.setAction(ACTION_SEARCH_INTERNAL);
- intent.putExtra(SHORTCUT_ACTION_KEY, mShortcutAction);
- if (mShortcutAction != null) {
- if (Intent.ACTION_CALL.equals(mShortcutAction)
- || Intent.ACTION_SENDTO.equals(mShortcutAction)) {
- intent.putExtra(Insert.PHONE, query);
- }
- } else {
- switch (mQueryMode) {
- case QUERY_MODE_MAILTO:
- intent.putExtra(Insert.EMAIL, query);
- break;
- case QUERY_MODE_TEL:
- intent.putExtra(Insert.PHONE, query);
- break;
- }
- }
+ intent.putExtra(ContactsSearchManager.ORIGINAL_REQUEST_KEY, mRequest);
+
+ if (returnResult) {
startActivityForResult(intent, SUBACTIVITY_SEARCH);
} else {
- intent.setAction(Intent.ACTION_SEARCH);
startActivity(intent);
}
}
@@ -1478,7 +683,7 @@
private void doShareVisibleContacts() {
final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
- sLookupProjection, getContactSelection(), null, null);
+ sLookupProjection, Contacts.IN_VISIBLE_GROUP + "!=0", null, null);
try {
if (!cursor.moveToFirst()) {
Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
@@ -1525,126 +730,48 @@
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
- case SUBACTIVITY_NEW_CONTACT:
- if (resultCode == RESULT_OK) {
- returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
- data.getData());
- }
- break;
+// case SUBACTIVITY_NEW_CONTACT:
+// if (resultCode == RESULT_OK) {
+// returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
+// data.getData());
+// setRe
+// }
+// break;
- case SUBACTIVITY_VIEW_CONTACT:
- if (resultCode == RESULT_OK) {
- mAdapter.notifyDataSetChanged();
- }
- break;
-
- case SUBACTIVITY_DISPLAY_GROUP:
- // Mark as just created so we re-run the view query
- mJustCreated = true;
- break;
-
- case SUBACTIVITY_FILTER:
- case SUBACTIVITY_SEARCH:
+// case SUBACTIVITY_VIEW_CONTACT:
+// if (resultCode == RESULT_OK) {
+// mAdapter.notifyDataSetChanged();
+// }
+// break;
+//
+// case SUBACTIVITY_DISPLAY_GROUP:
+// // Mark as just created so we re-run the view query
+//// mJustCreated = true;
+// break;
+//
+ case ContactEntryListFragment.ACTIVITY_REQUEST_CODE_FILTER:
+// case SUBACTIVITY_SEARCH:
// Pass through results of filter or search UI
if (resultCode == RESULT_OK) {
setResult(RESULT_OK, data);
finish();
}
- break;
+
+// TODO fix or remove multipicker code
+// 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;
}
}
@Override
- public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
- // If Contacts was invoked by another Activity simply as a way of
- // picking a contact, don't show the context menu
- if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
- return;
- }
-
- AdapterView.AdapterContextMenuInfo info;
- try {
- info = (AdapterView.AdapterContextMenuInfo) menuInfo;
- } catch (ClassCastException e) {
- Log.e(TAG, "bad menuInfo", e);
- return;
- }
-
- Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
- if (cursor == null) {
- // For some reason the requested item isn't available, do nothing
- return;
- }
- long id = info.id;
- Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id);
- long rawContactId = ContactsUtils.queryForRawContactId(getContentResolver(), id);
- Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
-
- // Setup the menu header
- menu.setHeaderTitle(cursor.getString(getSummaryDisplayNameColumnIndex()));
-
- // View contact details
- menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact)
- .setIntent(new Intent(Intent.ACTION_VIEW, contactUri));
-
- if (cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0) {
- // Calling contact
- menu.add(0, MENU_ITEM_CALL, 0,
- getString(R.string.menu_call));
- // Send SMS item
- menu.add(0, MENU_ITEM_SEND_SMS, 0, getString(R.string.menu_sendSMS));
- }
-
- // Star toggling
- int starState = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
- if (starState == 0) {
- menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar);
- } else {
- menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar);
- }
-
- // Contact editing
- menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact)
- .setIntent(new Intent(Intent.ACTION_EDIT, rawContactUri));
- menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact);
- }
-
- @Override
public boolean onContextItemSelected(MenuItem item) {
- AdapterView.AdapterContextMenuInfo info;
- try {
- info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
- } catch (ClassCastException e) {
- Log.e(TAG, "bad menuInfo", e);
- return false;
- }
-
- Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
-
- switch (item.getItemId()) {
- case MENU_ITEM_TOGGLE_STAR: {
- // Toggle the star
- ContentValues values = new ContentValues(1);
- values.put(Contacts.STARRED, cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX) == 0 ? 1 : 0);
- final Uri selectedUri = this.getContactUri(info.position);
- getContentResolver().update(selectedUri, values, null, null);
- return true;
- }
-
- case MENU_ITEM_CALL: {
- callContact(cursor);
- return true;
- }
-
- case MENU_ITEM_SEND_SMS: {
- smsContact(cursor);
- return true;
- }
-
- case MENU_ITEM_DELETE: {
- doContactDelete(getContactUri(info.position));
- return true;
- }
+ ContextMenuAdapter menuAdapter = mListFragment.getContextMenuAdapter();
+ if (menuAdapter != null) {
+ return menuAdapter.onContextItemSelected(item);
}
return super.onContextItemSelected(item);
@@ -1654,8 +781,10 @@
* Event handler for the use case where the user starts typing without
* bringing up the search UI first.
*/
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0 && !mSearchInitiated) {
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (!mSearchInitiated && !mRequest.isSearchMode()
+ && !mRequest.isSearchResultsMode()) {
int unicodeChar = event.getUnicodeChar();
if (unicodeChar != 0) {
mSearchInitiated = true;
@@ -1663,45 +792,19 @@
return true;
}
}
- return false;
- }
-
- /**
- * Event handler for search UI.
- */
- public void afterTextChanged(Editable s) {
- onSearchTextChanged();
- }
-
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- }
-
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- }
-
- /**
- * Event handler for search UI.
- */
- public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
- if (actionId == EditorInfo.IME_ACTION_DONE) {
- hideSoftKeyboard();
- if (TextUtils.isEmpty(getTextFilter())) {
- finish();
- }
- return true;
- }
- return false;
+ return super.dispatchKeyEvent(event);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // TODO move to the fragment
switch (keyCode) {
- case KeyEvent.KEYCODE_CALL: {
- if (callSelection()) {
- return true;
- }
- break;
- }
+// case KeyEvent.KEYCODE_CALL: {
+// if (callSelection()) {
+// return true;
+// }
+// break;
+// }
case KeyEvent.KEYCODE_DEL: {
if (deleteSelection()) {
@@ -1715,18 +818,17 @@
}
private boolean deleteSelection() {
- if ((mMode & MODE_MASK_PICKER) != 0) {
- return false;
- }
-
- final int position = getListView().getSelectedItemPosition();
- if (position != ListView.INVALID_POSITION) {
- Uri contactUri = getContactUri(position);
- if (contactUri != null) {
- doContactDelete(contactUri);
- return true;
- }
- }
+ // TODO move to the fragment
+// if (mActionCode == ContactsRequest.ACTION_DEFAULT) {
+// final int position = mListView.getSelectedItemPosition();
+// if (position != ListView.INVALID_POSITION) {
+// Uri contactUri = getContactUri(position);
+// if (contactUri != null) {
+// doContactDelete(contactUri);
+// return true;
+// }
+// }
+// }
return false;
}
@@ -1773,1813 +875,10 @@
}
}
- /**
- * Dismisses the soft keyboard when the list takes focus.
- */
- public void onFocusChange(View view, boolean hasFocus) {
- if (view == getListView() && hasFocus) {
- hideSoftKeyboard();
+ private CallOrSmsInitiator getCallOrSmsInitiator() {
+ if (mCallOrSmsInitiator == null) {
+ mCallOrSmsInitiator = new CallOrSmsInitiator(this);
}
- }
-
- /**
- * Dismisses the soft keyboard when the list takes focus.
- */
- public boolean onTouch(View view, MotionEvent event) {
- if (view == getListView()) {
- hideSoftKeyboard();
- }
- return false;
- }
-
- /**
- * Dismisses the search UI along with the keyboard if the filter text is empty.
- */
- public boolean onKeyPreIme(int keyCode, KeyEvent event) {
- if (mSearchMode && keyCode == KeyEvent.KEYCODE_BACK && TextUtils.isEmpty(getTextFilter())) {
- hideSoftKeyboard();
- onBackPressed();
- return true;
- }
- return false;
- }
-
- @Override
- protected void onListItemClick(ListView l, View v, int position, long id) {
- hideSoftKeyboard();
-
- if (mSearchMode && mAdapter.isSearchAllContactsItemPosition(position)) {
- doSearch();
- } else if (mMode == MODE_INSERT_OR_EDIT_CONTACT || mMode == MODE_QUERY_PICK_TO_EDIT) {
- Intent intent;
- if (position == 0 && !mSearchMode && mMode != MODE_QUERY_PICK_TO_EDIT) {
- intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
- } else {
- intent = new Intent(Intent.ACTION_EDIT, getSelectedUri(position));
- }
- intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
- Bundle extras = getIntent().getExtras();
- if (extras != null) {
- intent.putExtras(extras);
- }
- intent.putExtra(KEY_PICKER_MODE, (mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER);
-
- startActivity(intent);
- finish();
- } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
- && 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);
- startActivity(intent);
- finish();
- } else if (mMode == MODE_PICK_PHONE || mMode == MODE_QUERY_PICK_PHONE) {
- Cursor c = (Cursor) mAdapter.getItem(position);
- returnPickerResult(c, c.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX), uri);
- } else if ((mMode & MODE_MASK_PICKER) != 0) {
- Cursor c = (Cursor) mAdapter.getItem(position);
- returnPickerResult(c, c.getString(getSummaryDisplayNameColumnIndex()), uri);
- } else if (mMode == MODE_PICK_POSTAL
- || mMode == MODE_LEGACY_PICK_POSTAL
- || mMode == MODE_LEGACY_PICK_PHONE) {
- returnPickerResult(null, null, uri);
- }
- } else {
- signalError();
- }
- }
-
- private void hideSoftKeyboard() {
- // Hide soft keyboard, if visible
- InputMethodManager inputMethodManager = (InputMethodManager)
- getSystemService(Context.INPUT_METHOD_SERVICE);
- inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0);
- }
-
- /**
- * @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) {
- final Intent intent = new Intent();
-
- if (mShortcutAction != null) {
- Intent shortcutIntent;
- if (Intent.ACTION_VIEW.equals(mShortcutAction)) {
- // This is a simple shortcut to view a contact.
- shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT);
- shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
- Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
-
- shortcutIntent.setData(selectedUri);
- shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_MODE,
- ContactsContract.QuickContact.MODE_LARGE);
- shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES,
- (String[]) null);
-
- final Bitmap icon = framePhoto(loadContactPhoto(selectedUri, null));
- if (icon != null) {
- intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, scaleToAppIconSize(icon));
- } else {
- intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
- Intent.ShortcutIconResource.fromContext(this,
- R.drawable.ic_launcher_shortcut_contact));
- }
- } else {
- // This is a direct dial or sms shortcut.
- String number = c.getString(PHONE_NUMBER_COLUMN_INDEX);
- int type = c.getInt(PHONE_TYPE_COLUMN_INDEX);
- String scheme;
- int resid;
- if (Intent.ACTION_CALL.equals(mShortcutAction)) {
- scheme = Constants.SCHEME_TEL;
- resid = R.drawable.badge_action_call;
- } else {
- scheme = Constants.SCHEME_SMSTO;
- resid = R.drawable.badge_action_sms;
- }
-
- // Make the URI a direct tel: URI so that it will always continue to work
- Uri phoneUri = Uri.fromParts(scheme, number, null);
- shortcutIntent = new Intent(mShortcutAction, phoneUri);
-
- intent.putExtra(Intent.EXTRA_SHORTCUT_ICON,
- generatePhoneNumberIcon(selectedUri, type, resid));
- }
- shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
- intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
- setResult(RESULT_OK, intent);
- } else {
- intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
- setResult(RESULT_OK, intent.setData(selectedUri));
- }
- finish();
- }
-
- private Bitmap framePhoto(Bitmap photo) {
- final Resources r = getResources();
- final Drawable frame = r.getDrawable(com.android.internal.R.drawable.quickcontact_badge);
-
- final int width = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_width);
- final int height = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_height);
-
- frame.setBounds(0, 0, width, height);
-
- final Rect padding = new Rect();
- frame.getPadding(padding);
-
- final Rect source = new Rect(0, 0, photo.getWidth(), photo.getHeight());
- final Rect destination = new Rect(padding.left, padding.top,
- width - padding.right, height - padding.bottom);
-
- final int d = Math.max(width, height);
- final Bitmap b = Bitmap.createBitmap(d, d, Bitmap.Config.ARGB_8888);
- final Canvas c = new Canvas(b);
-
- c.translate((d - width) / 2.0f, (d - height) / 2.0f);
- frame.draw(c);
- c.drawBitmap(photo, source, destination, new Paint(Paint.FILTER_BITMAP_FLAG));
-
- return b;
- }
-
- /**
- * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
- * number, and if there is a photo also adds the call action icon.
- *
- * @param lookupUri The person the phone number belongs to
- * @param type The type of the phone number
- * @param actionResId The ID for the action resource
- * @return The bitmap for the icon
- */
- private Bitmap generatePhoneNumberIcon(Uri lookupUri, int type, int actionResId) {
- final Resources r = getResources();
- boolean drawPhoneOverlay = true;
- final float scaleDensity = getResources().getDisplayMetrics().scaledDensity;
-
- Bitmap photo = loadContactPhoto(lookupUri, null);
- if (photo == null) {
- // If there isn't a photo use the generic phone action icon instead
- Bitmap phoneIcon = getPhoneActionIcon(r, actionResId);
- if (phoneIcon != null) {
- photo = phoneIcon;
- drawPhoneOverlay = false;
- } else {
- return null;
- }
- }
-
- // Setup the drawing classes
- Bitmap icon = createShortcutBitmap();
- Canvas canvas = new Canvas(icon);
-
- // Copy in the photo
- Paint photoPaint = new Paint();
- photoPaint.setDither(true);
- photoPaint.setFilterBitmap(true);
- Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
- Rect dst = new Rect(0,0, mIconSize, mIconSize);
- canvas.drawBitmap(photo, src, dst, photoPaint);
-
- // Create an overlay for the phone number type
- String overlay = null;
- switch (type) {
- case Phone.TYPE_HOME:
- overlay = getString(R.string.type_short_home);
- break;
-
- case Phone.TYPE_MOBILE:
- overlay = getString(R.string.type_short_mobile);
- break;
-
- case Phone.TYPE_WORK:
- overlay = getString(R.string.type_short_work);
- break;
-
- case Phone.TYPE_PAGER:
- overlay = getString(R.string.type_short_pager);
- break;
-
- case Phone.TYPE_OTHER:
- overlay = getString(R.string.type_short_other);
- break;
- }
- if (overlay != null) {
- Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
- textPaint.setTextSize(20.0f * scaleDensity);
- textPaint.setTypeface(Typeface.DEFAULT_BOLD);
- textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
- textPaint.setShadowLayer(3f, 1, 1, r.getColor(R.color.textColorIconOverlayShadow));
- canvas.drawText(overlay, 2 * scaleDensity, 16 * scaleDensity, textPaint);
- }
-
- // Draw the phone action icon as an overlay
- if (ENABLE_ACTION_ICON_OVERLAYS && drawPhoneOverlay) {
- Bitmap phoneIcon = getPhoneActionIcon(r, actionResId);
- if (phoneIcon != null) {
- src.set(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
- int iconWidth = icon.getWidth();
- dst.set(iconWidth - ((int) (20 * scaleDensity)), -1,
- iconWidth, ((int) (19 * scaleDensity)));
- canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
- }
- }
-
- return icon;
- }
-
- private Bitmap scaleToAppIconSize(Bitmap photo) {
- // Setup the drawing classes
- Bitmap icon = createShortcutBitmap();
- Canvas canvas = new Canvas(icon);
-
- // Copy in the photo
- Paint photoPaint = new Paint();
- photoPaint.setDither(true);
- photoPaint.setFilterBitmap(true);
- Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
- Rect dst = new Rect(0,0, mIconSize, mIconSize);
- canvas.drawBitmap(photo, src, dst, photoPaint);
-
- return icon;
- }
-
- private Bitmap createShortcutBitmap() {
- return Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
- }
-
- /**
- * Returns the icon for the phone call action.
- *
- * @param r The resources to load the icon from
- * @param resId The resource ID to load
- * @return the icon for the phone call action
- */
- private Bitmap getPhoneActionIcon(Resources r, int resId) {
- Drawable phoneIcon = r.getDrawable(resId);
- if (phoneIcon instanceof BitmapDrawable) {
- BitmapDrawable bd = (BitmapDrawable) phoneIcon;
- return bd.getBitmap();
- } else {
- return null;
- }
- }
-
- private Uri getUriToQuery() {
- switch(mMode) {
- case MODE_JOIN_CONTACT:
- return getJoinSuggestionsUri(null);
- case MODE_FREQUENT:
- case MODE_STARRED:
- return Contacts.CONTENT_URI;
-
- case MODE_DEFAULT:
- case MODE_CUSTOM:
- case MODE_INSERT_OR_EDIT_CONTACT:
- case MODE_PICK_CONTACT:
- case MODE_PICK_OR_CREATE_CONTACT:{
- return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS;
- }
- case MODE_STREQUENT: {
- return Contacts.CONTENT_STREQUENT_URI;
- }
- case MODE_LEGACY_PICK_PERSON:
- case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
- return People.CONTENT_URI;
- }
- case MODE_PICK_PHONE: {
- return buildSectionIndexerUri(Phone.CONTENT_URI);
- }
- case MODE_LEGACY_PICK_PHONE: {
- return Phones.CONTENT_URI;
- }
- case MODE_PICK_POSTAL: {
- return buildSectionIndexerUri(StructuredPostal.CONTENT_URI);
- }
- case MODE_LEGACY_PICK_POSTAL: {
- return ContactMethods.CONTENT_URI;
- }
- case MODE_QUERY_PICK_TO_VIEW: {
- if (mQueryMode == QUERY_MODE_MAILTO) {
- return Uri.withAppendedPath(Email.CONTENT_FILTER_URI,
- Uri.encode(mInitialFilter));
- } else if (mQueryMode == QUERY_MODE_TEL) {
- return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI,
- Uri.encode(mInitialFilter));
- }
- return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS;
- }
- case MODE_QUERY:
- case MODE_QUERY_PICK:
- case MODE_QUERY_PICK_TO_EDIT: {
- return getContactFilterUri(mInitialFilter);
- }
- case MODE_QUERY_PICK_PHONE: {
- return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI,
- Uri.encode(mInitialFilter));
- }
- case MODE_GROUP: {
- return mGroupUri;
- }
- default: {
- throw new IllegalStateException("Can't generate URI: Unsupported Mode.");
- }
- }
- }
-
- /**
- * Build the {@link Contacts#CONTENT_LOOKUP_URI} for the given
- * {@link ListView} position, using {@link #mAdapter}.
- */
- private Uri getContactUri(int position) {
- if (position == ListView.INVALID_POSITION) {
- throw new IllegalArgumentException("Position not in list bounds");
- }
-
- final Cursor cursor = (Cursor)mAdapter.getItem(position);
- if (cursor == null) {
- return null;
- }
-
- switch(mMode) {
- case MODE_LEGACY_PICK_PERSON:
- case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
- final long personId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
- return ContentUris.withAppendedId(People.CONTENT_URI, personId);
- }
-
- default: {
- // Build and return soft, lookup reference
- final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
- final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX);
- return Contacts.getLookupUri(contactId, lookupKey);
- }
- }
- }
-
- /**
- * 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) {
- if (position == ListView.INVALID_POSITION) {
- throw new IllegalArgumentException("Position not in list bounds");
- }
-
- final long id = mAdapter.getItemId(position);
- switch(mMode) {
- case MODE_LEGACY_PICK_PERSON:
- case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
- return ContentUris.withAppendedId(People.CONTENT_URI, id);
- }
- case MODE_PICK_PHONE:
- case MODE_QUERY_PICK_PHONE: {
- return ContentUris.withAppendedId(Data.CONTENT_URI, id);
- }
- case MODE_LEGACY_PICK_PHONE: {
- return ContentUris.withAppendedId(Phones.CONTENT_URI, id);
- }
- case MODE_PICK_POSTAL: {
- return ContentUris.withAppendedId(Data.CONTENT_URI, id);
- }
- case MODE_LEGACY_PICK_POSTAL: {
- return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id);
- }
- default: {
- return getContactUri(position);
- }
- }
- }
-
- String[] getProjectionForQuery() {
- switch(mMode) {
- case MODE_JOIN_CONTACT:
- case MODE_STREQUENT:
- case MODE_FREQUENT:
- case MODE_STARRED:
- case MODE_DEFAULT:
- case MODE_CUSTOM:
- case MODE_INSERT_OR_EDIT_CONTACT:
- case MODE_GROUP:
- case MODE_PICK_CONTACT:
- case MODE_PICK_OR_CREATE_CONTACT: {
- return mSearchMode
- ? CONTACTS_SUMMARY_FILTER_PROJECTION
- : CONTACTS_SUMMARY_PROJECTION;
- }
- case MODE_QUERY:
- case MODE_QUERY_PICK:
- case MODE_QUERY_PICK_TO_EDIT: {
- return CONTACTS_SUMMARY_FILTER_PROJECTION;
- }
- case MODE_LEGACY_PICK_PERSON:
- case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
- return LEGACY_PEOPLE_PROJECTION ;
- }
- case MODE_QUERY_PICK_PHONE:
- case MODE_PICK_PHONE: {
- return PHONES_PROJECTION;
- }
- case MODE_LEGACY_PICK_PHONE: {
- return LEGACY_PHONES_PROJECTION;
- }
- case MODE_PICK_POSTAL: {
- return POSTALS_PROJECTION;
- }
- case MODE_LEGACY_PICK_POSTAL: {
- return LEGACY_POSTALS_PROJECTION;
- }
- case MODE_QUERY_PICK_TO_VIEW: {
- if (mQueryMode == QUERY_MODE_MAILTO) {
- return CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL;
- } else if (mQueryMode == QUERY_MODE_TEL) {
- return PHONES_PROJECTION;
- }
- break;
- }
- }
-
- // Default to normal aggregate projection
- return CONTACTS_SUMMARY_PROJECTION;
- }
-
- private Bitmap loadContactPhoto(Uri selectedUri, BitmapFactory.Options options) {
- Uri contactUri = null;
- if (Contacts.CONTENT_ITEM_TYPE.equals(getContentResolver().getType(selectedUri))) {
- // TODO we should have a "photo" directory under the lookup URI itself
- contactUri = Contacts.lookupContact(getContentResolver(), selectedUri);
- } else {
-
- Cursor cursor = getContentResolver().query(selectedUri,
- new String[] { Data.CONTACT_ID }, null, null, null);
- try {
- if (cursor != null && cursor.moveToFirst()) {
- final long contactId = cursor.getLong(0);
- contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
- }
- } finally {
- if (cursor != null) cursor.close();
- }
- }
-
- Cursor cursor = null;
- Bitmap bm = null;
-
- try {
- Uri photoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.CONTENT_DIRECTORY);
- cursor = getContentResolver().query(photoUri, new String[] {Photo.PHOTO},
- null, null, null);
- if (cursor != null && cursor.moveToFirst()) {
- bm = ContactsUtils.loadContactPhoto(cursor, 0, options);
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- if (bm == null) {
- final int[] fallbacks = {
- R.drawable.ic_contact_picture,
- R.drawable.ic_contact_picture_2,
- R.drawable.ic_contact_picture_3
- };
- bm = BitmapFactory.decodeResource(getResources(),
- fallbacks[new Random().nextInt(fallbacks.length)]);
- }
-
- return bm;
- }
-
- /**
- * Return the selection arguments for a default query based on the
- * {@link #mDisplayOnlyPhones} flag.
- */
- private String getContactSelection() {
- if (mDisplayOnlyPhones) {
- return CLAUSE_ONLY_VISIBLE + " AND " + CLAUSE_ONLY_PHONES;
- } else {
- return CLAUSE_ONLY_VISIBLE;
- }
- }
-
- private Uri getContactFilterUri(String filter) {
- Uri baseUri;
- if (!TextUtils.isEmpty(filter)) {
- baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter));
- } else {
- baseUri = Contacts.CONTENT_URI;
- }
-
- if (mAdapter.getDisplaySectionHeadersEnabled()) {
- return buildSectionIndexerUri(baseUri);
- } else {
- return baseUri;
- }
- }
-
- private Uri getPeopleFilterUri(String filter) {
- if (!TextUtils.isEmpty(filter)) {
- return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter));
- } else {
- return People.CONTENT_URI;
- }
- }
-
- private static Uri buildSectionIndexerUri(Uri uri) {
- return uri.buildUpon()
- .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) {
- if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
- return Contacts.SORT_KEY_PRIMARY;
- } else {
- return Contacts.SORT_KEY_ALTERNATIVE;
- }
- }
-
- 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);
- }
-
- mAdapter.setLoading(true);
-
- // Cancel any pending queries
- mQueryHandler.cancelOperation(QUERY_TOKEN);
- mQueryHandler.setLoadingJoinSuggestions(false);
-
- mSortOrder = mContactsPrefs.getSortOrder();
- mDisplayOrder = mContactsPrefs.getDisplayOrder();
-
- // When sort order and display order contradict each other, we want to
- // highlight the part of the name used for sorting.
- mHighlightWhenScrolling = false;
- if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY &&
- mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE) {
- mHighlightWhenScrolling = true;
- } else if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE &&
- mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
- mHighlightWhenScrolling = true;
- }
-
- String[] projection = getProjectionForQuery();
- if (mSearchMode && TextUtils.isEmpty(getTextFilter())) {
- mAdapter.changeCursor(new MatrixCursor(projection));
- return;
- }
-
- String callingPackage = getCallingPackage();
- Uri uri = getUriToQuery();
- if (!TextUtils.isEmpty(callingPackage)) {
- uri = uri.buildUpon()
- .appendQueryParameter(ContactsContract.REQUESTING_PACKAGE_PARAM_KEY,
- callingPackage)
- .build();
- }
-
- // Kick off the new query
- switch (mMode) {
- case MODE_GROUP:
- case MODE_DEFAULT:
- case MODE_CUSTOM:
- case MODE_PICK_CONTACT:
- case MODE_PICK_OR_CREATE_CONTACT:
- case MODE_INSERT_OR_EDIT_CONTACT:
- mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, getContactSelection(),
- null, getSortOrder(projection));
- break;
-
- case MODE_LEGACY_PICK_PERSON:
- case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
- mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null,
- People.DISPLAY_NAME);
- break;
- }
- case MODE_PICK_POSTAL:
- case MODE_QUERY:
- case MODE_QUERY_PICK:
- case MODE_QUERY_PICK_PHONE:
- case MODE_QUERY_PICK_TO_VIEW:
- case MODE_QUERY_PICK_TO_EDIT: {
- mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null,
- getSortOrder(projection));
- break;
- }
-
- case MODE_STARRED:
- mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
- projection, Contacts.STARRED + "=1", null,
- getSortOrder(projection));
- break;
-
- case MODE_FREQUENT:
- mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
- projection,
- Contacts.TIMES_CONTACTED + " > 0", null,
- Contacts.TIMES_CONTACTED + " DESC, "
- + getSortOrder(projection));
- break;
-
- case MODE_STREQUENT:
- mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, null);
- break;
-
- case MODE_PICK_PHONE:
- mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
- projection, CLAUSE_ONLY_VISIBLE, null, getSortOrder(projection));
- break;
-
- case MODE_LEGACY_PICK_PHONE:
- mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
- projection, null, null, Phones.DISPLAY_NAME);
- break;
-
- case MODE_LEGACY_PICK_POSTAL:
- mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
- projection,
- 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;
- }
- }
-
- /**
- * Called from a background thread to do the filter and return the resulting cursor.
- *
- * @param filter the text that was entered to filter on
- * @return a cursor with the results of the filter
- */
- Cursor doFilter(String filter) {
- String[] projection = getProjectionForQuery();
- if (mSearchMode && TextUtils.isEmpty(getTextFilter())) {
- return new MatrixCursor(projection);
- }
-
- final ContentResolver resolver = getContentResolver();
- switch (mMode) {
- case MODE_DEFAULT:
- case MODE_CUSTOM:
- case MODE_PICK_CONTACT:
- case MODE_PICK_OR_CREATE_CONTACT:
- case MODE_INSERT_OR_EDIT_CONTACT: {
- return resolver.query(getContactFilterUri(filter), projection,
- getContactSelection(), null, getSortOrder(projection));
- }
-
- case MODE_LEGACY_PICK_PERSON:
- case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
- return resolver.query(getPeopleFilterUri(filter), projection, null, null,
- People.DISPLAY_NAME);
- }
-
- case MODE_STARRED: {
- return resolver.query(getContactFilterUri(filter), projection,
- Contacts.STARRED + "=1", null,
- getSortOrder(projection));
- }
-
- case MODE_FREQUENT: {
- return resolver.query(getContactFilterUri(filter), projection,
- Contacts.TIMES_CONTACTED + " > 0", null,
- Contacts.TIMES_CONTACTED + " DESC, "
- + getSortOrder(projection));
- }
-
- case MODE_STREQUENT: {
- Uri uri;
- if (!TextUtils.isEmpty(filter)) {
- uri = Uri.withAppendedPath(Contacts.CONTENT_STREQUENT_FILTER_URI,
- Uri.encode(filter));
- } else {
- uri = Contacts.CONTENT_STREQUENT_URI;
- }
- return resolver.query(uri, projection, null, null, null);
- }
-
- case MODE_PICK_PHONE: {
- Uri uri = getUriToQuery();
- if (!TextUtils.isEmpty(filter)) {
- uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(filter));
- }
- return resolver.query(uri, projection, CLAUSE_ONLY_VISIBLE, null,
- getSortOrder(projection));
- }
-
- case MODE_LEGACY_PICK_PHONE: {
- //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.
- * @return true if the call was initiated, false otherwise
- */
- boolean callSelection() {
- ListView list = getListView();
- if (list.hasFocus()) {
- Cursor cursor = (Cursor) list.getSelectedItem();
- return callContact(cursor);
- }
- return false;
- }
-
- boolean callContact(Cursor cursor) {
- return callOrSmsContact(cursor, false /*call*/);
- }
-
- boolean smsContact(Cursor cursor) {
- return callOrSmsContact(cursor, true /*sms*/);
- }
-
- /**
- * Calls the contact which the cursor is point to.
- * @return true if the call was initiated, false otherwise
- */
- boolean callOrSmsContact(Cursor cursor, boolean sendSms) {
- if (cursor == null) {
- return false;
- }
-
- switch (mMode) {
- case MODE_PICK_PHONE:
- case MODE_LEGACY_PICK_PHONE:
- case MODE_QUERY_PICK_PHONE: {
- String phone = cursor.getString(PHONE_NUMBER_COLUMN_INDEX);
- if (sendSms) {
- ContactsUtils.initiateSms(this, phone);
- } else {
- ContactsUtils.initiateCall(this, phone);
- }
- return true;
- }
-
- case MODE_PICK_POSTAL:
- case MODE_LEGACY_PICK_POSTAL: {
- return false;
- }
-
- default: {
-
- boolean hasPhone = cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
- if (!hasPhone) {
- // There is no phone number.
- signalError();
- return false;
- }
-
- String phone = null;
- Cursor phonesCursor = null;
- phonesCursor = queryPhoneNumbers(cursor.getLong(SUMMARY_ID_COLUMN_INDEX));
- if (phonesCursor == null || phonesCursor.getCount() == 0) {
- // No valid number
- signalError();
- return false;
- } else if (phonesCursor.getCount() == 1) {
- // only one number, call it.
- phone = phonesCursor.getString(phonesCursor.getColumnIndex(Phone.NUMBER));
- } else {
- phonesCursor.moveToPosition(-1);
- while (phonesCursor.moveToNext()) {
- if (phonesCursor.getInt(phonesCursor.
- getColumnIndex(Phone.IS_SUPER_PRIMARY)) != 0) {
- // Found super primary, call it.
- phone = phonesCursor.
- getString(phonesCursor.getColumnIndex(Phone.NUMBER));
- break;
- }
- }
- }
-
- if (phone == null) {
- // Display dialog to choose a number to call.
- PhoneDisambigDialog phoneDialog = new PhoneDisambigDialog(
- this, phonesCursor, sendSms);
- phoneDialog.show();
- } else {
- if (sendSms) {
- ContactsUtils.initiateSms(this, phone);
- } else {
- ContactsUtils.initiateCall(this, phone);
- }
- }
- }
- }
- return true;
- }
-
- private Cursor queryPhoneNumbers(long contactId) {
- Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
- Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
-
- Cursor c = getContentResolver().query(dataUri,
- new String[] {Phone._ID, Phone.NUMBER, Phone.IS_SUPER_PRIMARY,
- RawContacts.ACCOUNT_TYPE, Phone.TYPE, Phone.LABEL},
- Data.MIMETYPE + "=?", new String[] {Phone.CONTENT_ITEM_TYPE}, null);
- if (c != null && c.moveToFirst()) {
- return c;
- }
- return null;
- }
-
- // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
- protected String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
- if (count == 0) {
- return getString(zeroResourceId);
- } else {
- String format = getResources().getQuantityText(pluralResourceId, count).toString();
- return String.format(format, count);
- }
- }
-
- /**
- * Signal an error to the user.
- */
- void signalError() {
- //TODO play an error beep or something...
- }
-
- Cursor getItemForView(View view) {
- ListView listView = getListView();
- int index = listView.getPositionForView(view);
- if (index < 0) {
- return null;
- }
- return (Cursor) listView.getAdapter().getItem(index);
- }
-
- private static class QueryHandler extends AsyncQueryHandler {
- protected final WeakReference<ContactsListActivity> mActivity;
- protected boolean mLoadingJoinSuggestions = false;
-
- public QueryHandler(Context context) {
- super(context.getContentResolver());
- mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context);
- }
-
- public void setLoadingJoinSuggestions(boolean flag) {
- mLoadingJoinSuggestions = flag;
- }
-
- @Override
- protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
- final ContactsListActivity activity = mActivity.get();
- if (activity != null && !activity.isFinishing()) {
-
- // Whenever we get a suggestions cursor, we need to immediately kick off
- // another query for the complete list of contacts
- if (cursor != null && mLoadingJoinSuggestions) {
- mLoadingJoinSuggestions = false;
- if (cursor.getCount() > 0) {
- activity.mAdapter.setSuggestionsCursor(cursor);
- } else {
- cursor.close();
- activity.mAdapter.setSuggestionsCursor(null);
- }
-
- if (activity.mAdapter.mSuggestionsCursorCount == 0
- || !activity.mJoinModeShowAllContacts) {
- startQuery(QUERY_TOKEN, null, activity.getContactFilterUri(
- activity.getTextFilter()),
- CONTACTS_SUMMARY_PROJECTION,
- Contacts._ID + " != " + activity.mQueryAggregateId
- + " AND " + CLAUSE_ONLY_VISIBLE, null,
- activity.getSortOrder(CONTACTS_SUMMARY_PROJECTION));
- return;
- }
-
- cursor = activity.getShowAllContactsLabelCursor(CONTACTS_SUMMARY_PROJECTION);
- }
-
- activity.mAdapter.changeCursor(cursor);
-
- // Now that the cursor is populated again, it's possible to restore the list state
- if (activity.mListState != null) {
- activity.mList.onRestoreInstanceState(activity.mListState);
- activity.mListState = null;
- }
- } else {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
- }
-
- 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);
- }
-
- final static class PinnedHeaderCache {
- public TextView titleView;
- public ColorStateList textColor;
- public Drawable background;
- }
-
- private final class ContactItemListAdapter extends CursorAdapter
- implements SectionIndexer, OnScrollListener, PinnedHeaderListView.PinnedHeaderAdapter {
- private SectionIndexer mIndexer;
- private boolean mLoading = true;
- private CharSequence mUnknownNameText;
- private boolean mDisplayPhotos = false;
- private boolean mDisplayCallButton = false;
- 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);
-
- mUnknownNameText = context.getText(android.R.string.unknownName);
- switch (mMode) {
- case MODE_LEGACY_PICK_POSTAL:
- case MODE_PICK_POSTAL:
- case MODE_LEGACY_PICK_PHONE:
- case MODE_PICK_PHONE:
- case MODE_STREQUENT:
- case MODE_FREQUENT:
- mDisplaySectionHeaders = false;
- break;
- }
-
- if (mSearchMode) {
- mDisplaySectionHeaders = false;
- }
-
- // Do not display the second line of text if in a specific SEARCH query mode, usually for
- // matching a specific E-mail or phone number. Any contact details
- // shown would be identical, and columns might not even be present
- // in the returned cursor.
- if (mMode != MODE_QUERY_PICK_PHONE && mQueryMode != QUERY_MODE_NONE) {
- mDisplayAdditionalData = false;
- }
-
- if ((mMode & MODE_MASK_NO_DATA) == MODE_MASK_NO_DATA) {
- mDisplayAdditionalData = false;
- }
-
- if ((mMode & MODE_MASK_SHOW_CALL_BUTTON) == MODE_MASK_SHOW_CALL_BUTTON) {
- mDisplayCallButton = true;
- }
-
- if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
- mDisplayPhotos = true;
- }
- }
-
- public boolean getDisplaySectionHeadersEnabled() {
- 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
- * block the UI thread for a long time.
- */
- @Override
- protected void onContentChanged() {
- CharSequence constraint = getTextFilter();
- if (!TextUtils.isEmpty(constraint)) {
- // Reset the filter state then start an async filter operation
- Filter filter = getFilter();
- filter.filter(constraint);
- } else {
- // Start an async query
- startQuery();
- }
- }
-
- public void setLoading(boolean loading) {
- mLoading = loading;
- }
-
- @Override
- public boolean isEmpty() {
- if (mProviderStatus != ProviderStatus.STATUS_NORMAL) {
- return true;
- }
-
- if (mSearchMode) {
- return TextUtils.isEmpty(getTextFilter());
- } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) {
- // This mode mask adds a header and we always want it to show up, even
- // if the list is empty, so always claim the list is not empty.
- return false;
- } else {
- if (mCursor == null || mLoading) {
- // We don't want the empty state to show when loading.
- return false;
- } else {
- return super.isEmpty();
- }
- }
- }
-
- @Override
- public int getItemViewType(int position) {
- if (position == 0 && (mShowNumberOfContacts || (mMode & MODE_MASK_CREATE_NEW) != 0)) {
- return IGNORE_ITEM_VIEW_TYPE;
- }
-
- if (isShowAllContactsItemPosition(position)) {
- return IGNORE_ITEM_VIEW_TYPE;
- }
-
- if (isSearchAllContactsItemPosition(position)) {
- return IGNORE_ITEM_VIEW_TYPE;
- }
-
- if (getSeparatorId(position) != 0) {
- // We don't want the separator view to be recycled.
- return IGNORE_ITEM_VIEW_TYPE;
- }
-
- return super.getItemViewType(position);
- }
-
- @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");
- }
-
- // handle the total contacts item
- if (position == 0 && mShowNumberOfContacts) {
- return getTotalContactCountView(parent);
- }
-
- if (position == 0 && (mMode & MODE_MASK_CREATE_NEW) != 0) {
- // Add the header for creating a new contact
- 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);
- }
-
- // 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, mDisplaySectionHeaders && !showingSuggestion);
- return v;
- }
-
- private View getTotalContactCountView(ViewGroup parent) {
- final LayoutInflater inflater = getLayoutInflater();
- View view = inflater.inflate(R.layout.total_contacts, parent, false);
-
- TextView totalContacts = (TextView) view.findViewById(R.id.totalContactsText);
-
- String text;
- int count = getRealCount();
-
- if (mSearchMode && !TextUtils.isEmpty(getTextFilter())) {
- text = getQuantityText(count, R.string.listFoundAllContactsZero,
- R.plurals.searchFoundContacts);
- } else {
- if (mDisplayOnlyPhones) {
- text = getQuantityText(count, R.string.listTotalPhoneContactsZero,
- R.plurals.listTotalPhoneContacts);
- } else {
- text = getQuantityText(count, R.string.listTotalAllContactsZero,
- R.plurals.listTotalAllContacts);
- }
- }
- totalContacts.setText(text);
- 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;
- }
-
- private int getSeparatorId(int position) {
- int separatorId = 0;
- 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;
- }
-
- @Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- final ContactListItemView view = new ContactListItemView(context, null);
- view.setOnCallButtonClickListener(ContactsListActivity.this);
- view.setTag(new ContactListItemCache());
- return view;
- }
-
- @Override
- public void bindView(View itemView, Context context, Cursor cursor) {
- final ContactListItemView view = (ContactListItemView)itemView;
- final ContactListItemCache cache = (ContactListItemCache) view.getTag();
-
- int typeColumnIndex;
- int dataColumnIndex;
- int labelColumnIndex;
- int defaultType;
- int nameColumnIndex;
- int phoneticNameColumnIndex;
- boolean displayAdditionalData = mDisplayAdditionalData;
- boolean highlightingEnabled = false;
- switch(mMode) {
- case MODE_PICK_PHONE:
- case MODE_LEGACY_PICK_PHONE:
- case MODE_QUERY_PICK_PHONE: {
- nameColumnIndex = PHONE_DISPLAY_NAME_COLUMN_INDEX;
- phoneticNameColumnIndex = -1;
- dataColumnIndex = PHONE_NUMBER_COLUMN_INDEX;
- typeColumnIndex = PHONE_TYPE_COLUMN_INDEX;
- labelColumnIndex = PHONE_LABEL_COLUMN_INDEX;
- defaultType = Phone.TYPE_HOME;
- break;
- }
- case MODE_PICK_POSTAL:
- case MODE_LEGACY_PICK_POSTAL: {
- nameColumnIndex = POSTAL_DISPLAY_NAME_COLUMN_INDEX;
- phoneticNameColumnIndex = -1;
- dataColumnIndex = POSTAL_ADDRESS_COLUMN_INDEX;
- typeColumnIndex = POSTAL_TYPE_COLUMN_INDEX;
- labelColumnIndex = POSTAL_LABEL_COLUMN_INDEX;
- defaultType = StructuredPostal.TYPE_HOME;
- break;
- }
- default: {
- nameColumnIndex = getSummaryDisplayNameColumnIndex();
- if (mMode == MODE_LEGACY_PICK_PERSON
- || mMode == MODE_LEGACY_PICK_OR_CREATE_PERSON) {
- phoneticNameColumnIndex = -1;
- } else {
- phoneticNameColumnIndex = SUMMARY_PHONETIC_NAME_COLUMN_INDEX;
- }
- dataColumnIndex = -1;
- typeColumnIndex = -1;
- labelColumnIndex = -1;
- defaultType = Phone.TYPE_HOME;
- displayAdditionalData = false;
- highlightingEnabled = mHighlightWhenScrolling && mMode != MODE_STREQUENT;
- }
- }
-
- // Set the name
- cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer);
- TextView nameView = view.getNameTextView();
- int size = cache.nameBuffer.sizeCopied;
- if (size != 0) {
- if (highlightingEnabled) {
- if (cache.textWithHighlighting == null) {
- cache.textWithHighlighting =
- mHighlightingAnimation.createTextWithHighlighting();
- }
- buildDisplayNameWithHighlighting(nameView, cursor, cache.nameBuffer,
- cache.highlightedTextBuffer, cache.textWithHighlighting);
- } else {
- nameView.setText(cache.nameBuffer.data, 0, size);
- }
- } else {
- 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) {
- int pos = cursor.getPosition();
- view.showCallButton(android.R.id.button1, pos);
- } else {
- view.hideCallButton();
- }
-
- // Set the photo, if requested
- if (mDisplayPhotos) {
- 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);
- }
-
- ImageView viewToUse;
- if (useQuickContact) {
- // Build soft lookup reference
- final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
- final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX);
- QuickContactBadge quickContact = view.getQuickContact();
- quickContact.assignContactUri(Contacts.getLookupUri(contactId, lookupKey));
- viewToUse = quickContact;
- } else {
- viewToUse = view.getPhotoView();
- }
-
- final int position = cursor.getPosition();
- mPhotoLoader.loadPhoto(viewToUse, photoId);
- }
-
- if ((mMode & MODE_MASK_NO_PRESENCE) == 0) {
- // Set the proper icon (star or presence or nothing)
- int serverStatus;
- if (!cursor.isNull(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX)) {
- serverStatus = cursor.getInt(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX);
- Drawable icon = ContactPresenceIconUtil.getPresenceIcon(mContext, serverStatus);
- if (icon != null) {
- view.setPresence(icon);
- } else {
- view.setPresence(null);
- }
- } else {
- view.setPresence(null);
- }
- } else {
- view.setPresence(null);
- }
-
- if (mShowSearchSnippets) {
- boolean showSnippet = false;
- String snippetMimeType = cursor.getString(SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX);
- if (Email.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
- String email = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX);
- if (!TextUtils.isEmpty(email)) {
- view.setSnippet(email);
- showSnippet = true;
- }
- } else if (Organization.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
- String company = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX);
- String title = cursor.getString(SUMMARY_SNIPPET_DATA4_COLUMN_INDEX);
- if (!TextUtils.isEmpty(company)) {
- if (!TextUtils.isEmpty(title)) {
- view.setSnippet(company + " / " + title);
- } else {
- view.setSnippet(company);
- }
- showSnippet = true;
- } else if (!TextUtils.isEmpty(title)) {
- view.setSnippet(title);
- showSnippet = true;
- }
- } else if (Nickname.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
- String nickname = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX);
- if (!TextUtils.isEmpty(nickname)) {
- view.setSnippet(nickname);
- showSnippet = true;
- }
- }
-
- if (!showSnippet) {
- view.setSnippet(null);
- }
- }
-
- if (!displayAdditionalData) {
- if (phoneticNameColumnIndex != -1) {
-
- // Set the name
- cursor.copyStringToBuffer(phoneticNameColumnIndex, cache.phoneticNameBuffer);
- int phoneticNameSize = cache.phoneticNameBuffer.sizeCopied;
- if (phoneticNameSize != 0) {
- view.setLabel(cache.phoneticNameBuffer.data, phoneticNameSize);
- } else {
- view.setLabel(null);
- }
- } else {
- view.setLabel(null);
- }
- return;
- }
-
- // Set the data.
- cursor.copyStringToBuffer(dataColumnIndex, cache.dataBuffer);
-
- size = cache.dataBuffer.sizeCopied;
- view.setData(cache.dataBuffer.data, size);
-
- // Set the label.
- if (!cursor.isNull(typeColumnIndex)) {
- final int type = cursor.getInt(typeColumnIndex);
- final String label = cursor.getString(labelColumnIndex);
-
- if (mMode == MODE_LEGACY_PICK_POSTAL || mMode == MODE_PICK_POSTAL) {
- // TODO cache
- view.setLabel(StructuredPostal.getTypeLabel(context.getResources(), type,
- label));
- } else {
- // TODO cache
- view.setLabel(Phone.getTypeLabel(context.getResources(), type, label));
- }
- } else {
- view.setLabel(null);
- }
- }
-
- /**
- * Computes the span of the display name that has highlighted parts and configures
- * the display name text view accordingly.
- */
- private void buildDisplayNameWithHighlighting(TextView textView, Cursor cursor,
- CharArrayBuffer buffer1, CharArrayBuffer buffer2,
- TextWithHighlighting textWithHighlighting) {
- int oppositeDisplayOrderColumnIndex;
- if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
- oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
- } else {
- oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
- }
- cursor.copyStringToBuffer(oppositeDisplayOrderColumnIndex, buffer2);
-
- textWithHighlighting.setText(buffer1, buffer2);
- textView.setText(textWithHighlighting);
- }
-
- private void bindSectionHeader(View itemView, int position, boolean displaySectionHeaders) {
- final ContactListItemView view = (ContactListItemView)itemView;
- final ContactListItemCache cache = (ContactListItemCache) view.getTag();
- if (!displaySectionHeaders) {
- view.setSectionHeader(null);
- view.setDividerVisible(true);
- } else {
- final int section = getSectionForPosition(position);
- if (getPositionForSection(section) == position) {
- String title = (String)mIndexer.getSections()[section];
- view.setSectionHeader(title);
- } else {
- view.setDividerVisible(false);
- view.setSectionHeader(null);
- }
-
- // move the divider for the last item in a section
- if (getPositionForSection(section + 1) - 1 == position) {
- view.setDividerVisible(false);
- } else {
- view.setDividerVisible(true);
- }
- }
- }
-
- @Override
- public void changeCursor(Cursor cursor) {
- if (cursor != null) {
- setLoading(false);
- }
-
- // Get the split between starred and frequent items, if the mode is strequent
- mFrequentSeparatorPos = ListView.INVALID_POSITION;
- int cursorCount = 0;
- if (cursor != null && (cursorCount = cursor.getCount()) > 0
- && mMode == MODE_STREQUENT) {
- cursor.move(-1);
- for (int i = 0; cursor.moveToNext(); i++) {
- int starred = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
- if (starred == 0) {
- if (i > 0) {
- // Only add the separator when there are starred items present
- mFrequentSeparatorPos = i;
- }
- break;
- }
- }
- }
-
- if (cursor != null && mSearchResultsMode) {
- TextView foundContactsText = (TextView)findViewById(R.id.search_results_found);
- String text = getQuantityText(cursor.getCount(), R.string.listFoundAllContactsZero,
- R.plurals.listFoundAllContacts);
- foundContactsText.setText(text);
- }
-
- super.changeCursor(cursor);
- // Update the indexer for the fast scroll widget
- updateIndexer(cursor);
- }
-
- private void updateIndexer(Cursor cursor) {
- if (cursor == null) {
- mIndexer = null;
- return;
- }
-
- Bundle bundle = cursor.getExtras();
- if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) {
- String sections[] =
- bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
- int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
- mIndexer = new ContactsSectionIndexer(sections, counts);
- } else {
- mIndexer = null;
- }
- }
-
- /**
- * Run the query on a helper thread. Beware that this code does not run
- * on the main UI thread!
- */
- @Override
- public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
- return doFilter(constraint.toString());
- }
-
- public Object [] getSections() {
- if (mIndexer == null) {
- return new String[] { " " };
- } else {
- return mIndexer.getSections();
- }
- }
-
- public int getPositionForSection(int sectionIndex) {
- if (mIndexer == null) {
- return -1;
- }
-
- return mIndexer.getPositionForSection(sectionIndex);
- }
-
- public int getSectionForPosition(int position) {
- if (mIndexer == null) {
- return -1;
- }
-
- return mIndexer.getSectionForPosition(position);
- }
-
- @Override
- public boolean areAllItemsEnabled() {
- return mMode != MODE_STARRED
- && !mShowNumberOfContacts
- && mSuggestionsCursorCount == 0;
- }
-
- @Override
- public boolean isEnabled(int position) {
- if (mShowNumberOfContacts) {
- if (position == 0) {
- return false;
- }
- position--;
- }
-
- if (mSuggestionsCursorCount > 0) {
- return position != 0 && position != mSuggestionsCursorCount + 1;
- }
- return position != mFrequentSeparatorPos;
- }
-
- @Override
- public int getCount() {
- if (!mDataValid) {
- return 0;
- }
- int superCount = super.getCount();
-
- if (mShowNumberOfContacts && (mSearchMode || superCount > 0)) {
- // We don't want to count this header if it's the only thing visible, so that
- // the empty text will display.
- superCount++;
- }
-
- if (mSearchMode) {
- // Last element in the list is the "Find
- superCount++;
- }
-
- // We do not show the "Create New" button in Search mode
- if ((mMode & MODE_MASK_CREATE_NEW) != 0 && !mSearchMode) {
- // Count the "Create new contact" line
- superCount++;
- }
-
- if (mSuggestionsCursorCount != 0) {
- // When showing suggestions, we have 2 additional list items: the "Suggestions"
- // and "All contacts" headers.
- return mSuggestionsCursorCount + superCount + 2;
- }
- else if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
- // When showing strequent list, we have an additional list item - the separator.
- return superCount + 1;
- } else {
- return superCount;
- }
- }
-
- /**
- * Gets the actual count of contacts and excludes all the headers.
- */
- public int getRealCount() {
- return super.getCount();
- }
-
- private int getRealPosition(int pos) {
- if (mShowNumberOfContacts) {
- pos--;
- }
-
- 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) {
- // No separator, identity map
- return pos;
- } else if (pos <= mFrequentSeparatorPos) {
- // Before or at the separator, identity map
- return pos;
- } else {
- // After the separator, remove 1 from the pos to get the real underlying pos
- return pos - 1;
- }
- }
-
- @Override
- public Object getItem(int pos) {
- if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) {
- mSuggestionsCursor.moveToPosition(getRealPosition(pos));
- return mSuggestionsCursor;
- } else if (isSearchAllContactsItemPosition(pos)){
- return null;
- } 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;
- }
- } else if (isSearchAllContactsItemPosition(pos)) {
- return 0;
- }
- int realPosition = getRealPosition(pos);
- if (realPosition < 0) {
- return 0;
- }
- return super.getItemId(realPosition);
- }
-
- public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
- int totalItemCount) {
- if (view instanceof PinnedHeaderListView) {
- ((PinnedHeaderListView)view).configureHeaderView(firstVisibleItem);
- }
- }
-
- public void onScrollStateChanged(AbsListView view, int scrollState) {
- if (mHighlightWhenScrolling) {
- if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
- mHighlightingAnimation.startHighlighting();
- } else {
- mHighlightingAnimation.stopHighlighting();
- }
- }
-
- if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
- mPhotoLoader.pause();
- } else if (mDisplayPhotos) {
- mPhotoLoader.resume();
- }
- }
-
- /**
- * Computes the state of the pinned header. It can be invisible, fully
- * visible or partially pushed up out of the view.
- */
- public int getPinnedHeaderState(int position) {
- if (mIndexer == null || mCursor == null || mCursor.getCount() == 0) {
- return PINNED_HEADER_GONE;
- }
-
- int realPosition = getRealPosition(position);
- if (realPosition < 0) {
- return PINNED_HEADER_GONE;
- }
-
- // The header should get pushed up if the top item shown
- // is the last item in a section for a particular letter.
- int section = getSectionForPosition(realPosition);
- int nextSectionPosition = getPositionForSection(section + 1);
- if (nextSectionPosition != -1 && realPosition == nextSectionPosition - 1) {
- return PINNED_HEADER_PUSHED_UP;
- }
-
- return PINNED_HEADER_VISIBLE;
- }
-
- /**
- * Configures the pinned header by setting the appropriate text label
- * and also adjusting color if necessary. The color needs to be
- * adjusted when the pinned header is being pushed up from the view.
- */
- public void configurePinnedHeader(View header, int position, int alpha) {
- PinnedHeaderCache cache = (PinnedHeaderCache)header.getTag();
- if (cache == null) {
- cache = new PinnedHeaderCache();
- cache.titleView = (TextView)header.findViewById(R.id.header_text);
- cache.textColor = cache.titleView.getTextColors();
- cache.background = header.getBackground();
- header.setTag(cache);
- }
-
- int realPosition = getRealPosition(position);
- int section = getSectionForPosition(realPosition);
-
- String title = (String)mIndexer.getSections()[section];
- cache.titleView.setText(title);
-
- if (alpha == 255) {
- // Opaque: use the default background, and the original text color
- header.setBackgroundDrawable(cache.background);
- cache.titleView.setTextColor(cache.textColor);
- } else {
- // Faded: use a solid color approximation of the background, and
- // a translucent text color
- header.setBackgroundColor(Color.rgb(
- Color.red(mPinnedHeaderBackgroundColor) * alpha / 255,
- Color.green(mPinnedHeaderBackgroundColor) * alpha / 255,
- Color.blue(mPinnedHeaderBackgroundColor) * alpha / 255));
-
- int textColor = cache.textColor.getDefaultColor();
- cache.titleView.setTextColor(Color.argb(alpha,
- Color.red(textColor), Color.green(textColor), Color.blue(textColor)));
- }
- }
+ return mCallOrSmsInitiator;
}
}
diff --git a/src/com/android/contacts/ContactsSearchActivity.java b/src/com/android/contacts/ContactsSearchActivity.java
new file mode 100644
index 0000000..3f1e62c
--- /dev/null
+++ b/src/com/android/contacts/ContactsSearchActivity.java
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+public class ContactsSearchActivity extends ContactsListActivity {
+
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ContactsSearchManager.java b/src/com/android/contacts/ContactsSearchManager.java
index d65e079..340d7d6 100644
--- a/src/com/android/contacts/ContactsSearchManager.java
+++ b/src/com/android/contacts/ContactsSearchManager.java
@@ -16,6 +16,8 @@
package com.android.contacts;
+import com.android.contacts.list.ContactsRequest;
+
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
@@ -40,18 +42,39 @@
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";
+
+ /**
+ * An extra that provides context for search UI and defines the scope for
+ * the search queries.
+ */
+ public static final String ORIGINAL_ACTION_CODE_EXTRA_KEY = "originalActionCode";
+
+ public static final String ORIGINAL_REQUEST_KEY = "originalRequest";
+
+ /**
* 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, ContactsRequest originalRequest) {
+ context.startActivityForResult(
+ buildIntent(context, initialQuery, originalRequest), requestCode);
}
- private static Intent buildIntent(Activity context, String initialQuery) {
+ public static void startSearch(Activity context, String initialQuery,
+ ContactsRequest originalRequest) {
+ context.startActivity(buildIntent(context, initialQuery, originalRequest));
+ }
+
+ private static Intent buildIntent(
+ Activity context, String initialQuery, ContactsRequest originalRequest) {
Intent intent = new Intent();
intent.setData(ContactsContract.Contacts.CONTENT_URI);
intent.setAction(UI.FILTER_CONTACTS_ACTION);
@@ -62,8 +85,9 @@
intent.putExtras(originalExtras);
}
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());
+ if (originalRequest != null) {
+ intent.putExtra(ORIGINAL_REQUEST_KEY, originalRequest);
+ }
return intent;
}
}
diff --git a/src/com/android/contacts/ImportVCardActivity.java b/src/com/android/contacts/ImportVCardActivity.java
deleted file mode 100644
index 0a324fe..0000000
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ /dev/null
@@ -1,984 +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.accounts.Account;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.ProgressDialog;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-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.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;
-import android.text.style.RelativeSizeSpan;
-import android.util.Log;
-
-import com.android.contacts.model.Sources;
-import com.android.contacts.util.AccountSelectionUtil;
-
-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;
-
-class VCardFile {
- private String mName;
- private String mCanonicalPath;
- private long mLastModified;
-
- public VCardFile(String name, String canonicalPath, long lastModified) {
- mName = name;
- mCanonicalPath = canonicalPath;
- mLastModified = lastModified;
- }
-
- public String getName() {
- return mName;
- }
-
- public String getCanonicalPath() {
- return mCanonicalPath;
- }
-
- public long getLastModified() {
- return mLastModified;
- }
-}
-
-/**
- * Class for importing vCard. Several user interaction will be required while reading
- * (selecting a file, waiting a moment, etc.)
- *
- * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
- * finished (with the method {@link Activity#finish()}) after the import and never reuse
- * any Dialog in the instance. So this code is careless about the management around managed
- * dialogs stuffs (like how onCreateDialog() is used).
- */
-public class ImportVCardActivity extends Activity {
- private static final String LOG_TAG = "ImportVCardActivity";
- private static final boolean DO_PERFORMANCE_PROFILE = false;
-
- // 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 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;
- public DialogDisplayer(int resId) {
- mResId = resId;
- }
- public DialogDisplayer(String errorMessage) {
- mResId = R.id.dialog_error_with_message;
- mErrorMessage = errorMessage;
- }
- public void run() {
- showDialog(mResId);
- }
- }
-
- private class CancelListener
- implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
- public void onClick(DialogInterface dialog, int which) {
- finish();
- }
-
- public void onCancel(DialogInterface dialog) {
- finish();
- }
- }
-
- 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;
- public static final int IMPORT_MULTIPLE = 1;
- public static final int IMPORT_ALL = 2;
- public static final int IMPORT_TYPE_SIZE = 3;
-
- private int mCurrentIndex;
-
- public void onClick(DialogInterface dialog, int which) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- switch (mCurrentIndex) {
- case IMPORT_ALL:
- importMultipleVCardFromSDCard(mAllVCardFileList);
- break;
- case IMPORT_MULTIPLE:
- showDialog(R.id.dialog_select_multiple_vcard);
- break;
- default:
- showDialog(R.id.dialog_select_one_vcard);
- break;
- }
- } else if (which == DialogInterface.BUTTON_NEGATIVE) {
- finish();
- } else {
- mCurrentIndex = which;
- }
- }
- }
-
- private class VCardSelectedListener implements
- DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener {
- private int mCurrentIndex;
- private Set<Integer> mSelectedIndexSet;
-
- public VCardSelectedListener(boolean multipleSelect) {
- mCurrentIndex = 0;
- if (multipleSelect) {
- mSelectedIndexSet = new HashSet<Integer>();
- }
- }
-
- public void onClick(DialogInterface dialog, int which) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- if (mSelectedIndexSet != null) {
- List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>();
- 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);
- } else {
- String canonicalPath = mAllVCardFileList.get(mCurrentIndex).getCanonicalPath();
- final Uri uri = Uri.parse("file://" + canonicalPath);
- importOneVCardFromSDCard(uri);
- }
- } else if (which == DialogInterface.BUTTON_NEGATIVE) {
- finish();
- } else {
- // Some file is selected.
- mCurrentIndex = which;
- if (mSelectedIndexSet != null) {
- if (mSelectedIndexSet.contains(which)) {
- mSelectedIndexSet.remove(which);
- } else {
- mSelectedIndexSet.add(which);
- }
- }
- }
- }
-
- public void onClick(DialogInterface dialog, int which, boolean isChecked) {
- if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) {
- Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which,
- mAllVCardFileList.get(which).getCanonicalPath()));
- } else {
- onClick(dialog, which);
- }
- }
- }
-
- /**
- * Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select
- * a vCard file is shown. After the choice, VCardReadThread starts running.
- */
- private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener {
- private boolean mCanceled;
- private boolean mGotIOException;
- private File mRootDirectory;
-
- // To avoid recursive link.
- private Set<String> mCheckedPaths;
- private PowerManager.WakeLock mWakeLock;
-
- private class CanceledException extends Exception {
- }
-
- public VCardScanThread(File sdcardDirectory) {
- mCanceled = false;
- mGotIOException = false;
- mRootDirectory = sdcardDirectory;
- mCheckedPaths = new HashSet<String>();
- PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService(
- Context.POWER_SERVICE);
- mWakeLock = powerManager.newWakeLock(
- PowerManager.SCREEN_DIM_WAKE_LOCK |
- PowerManager.ON_AFTER_RELEASE, LOG_TAG);
- }
-
- @Override
- public void run() {
- mAllVCardFileList = new Vector<VCardFile>();
- try {
- mWakeLock.acquire();
- getVCardFileRecursively(mRootDirectory);
- } catch (CanceledException e) {
- mCanceled = true;
- } catch (IOException e) {
- mGotIOException = true;
- } finally {
- mWakeLock.release();
- }
-
- if (mCanceled) {
- mAllVCardFileList = null;
- }
-
- mProgressDialogForScanVCard.dismiss();
- mProgressDialogForScanVCard = null;
-
- if (mGotIOException) {
- runOnUIThread(new DialogDisplayer(R.id.dialog_io_exception));
- } else if (mCanceled) {
- finish();
- } else {
- int size = mAllVCardFileList.size();
- final Context context = ImportVCardActivity.this;
- if (size == 0) {
- runOnUIThread(new DialogDisplayer(R.id.dialog_vcard_not_found));
- } else {
- startVCardSelectAndImport();
- }
- }
- }
-
- private void getVCardFileRecursively(File directory)
- throws CanceledException, IOException {
- if (mCanceled) {
- throw new CanceledException();
- }
-
- // e.g. secured directory may return null toward listFiles().
- final File[] files = directory.listFiles();
- if (files == null) {
- Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")");
- return;
- }
- for (File file : directory.listFiles()) {
- if (mCanceled) {
- throw new CanceledException();
- }
- String canonicalPath = file.getCanonicalPath();
- if (mCheckedPaths.contains(canonicalPath)) {
- continue;
- }
-
- mCheckedPaths.add(canonicalPath);
-
- if (file.isDirectory()) {
- getVCardFileRecursively(file);
- } else if (canonicalPath.toLowerCase().endsWith(".vcf") &&
- file.canRead()){
- String fileName = file.getName();
- VCardFile vcardFile = new VCardFile(
- fileName, canonicalPath, file.lastModified());
- mAllVCardFileList.add(vcardFile);
- }
- }
- }
-
- public void onCancel(DialogInterface dialog) {
- mCanceled = true;
- }
-
- public void onClick(DialogInterface dialog, int which) {
- if (which == DialogInterface.BUTTON_NEGATIVE) {
- mCanceled = true;
- }
- }
- }
-
- 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);
- } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) {
- runOnUIThread(new DialogDisplayer(R.id.dialog_select_import_type));
- } else {
- runOnUIThread(new DialogDisplayer(R.id.dialog_select_one_vcard));
- }
- }
-
- 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 importOneVCardFromSDCard(final Uri uri) {
- runOnUIThread(new Runnable() {
- public void run() {
- mVCardReadThread = new VCardReadThread(uri);
- showDialog(R.id.dialog_reading_vcard);
- }
- });
- }
-
- 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);
-
- String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE];
- items[ImportTypeSelectedListener.IMPORT_ONE] =
- getString(R.string.import_one_vcard_string);
- items[ImportTypeSelectedListener.IMPORT_MULTIPLE] =
- getString(R.string.import_multiple_vcard_string);
- items[ImportTypeSelectedListener.IMPORT_ALL] =
- 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);
-
- CharSequence[] items = new CharSequence[size];
- DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- for (int i = 0; i < size; i++) {
- VCardFile vcardFile = mAllVCardFileList.get(i);
- SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
- stringBuilder.append(vcardFile.getName());
- stringBuilder.append('\n');
- int indexToBeSpanned = stringBuilder.length();
- // Smaller date text looks better, since each file name becomes easier to read.
- // The value set to RelativeSizeSpan is arbitrary. You can change it to any other
- // value (but the value bigger than 1.0f would not make nice appearance :)
- stringBuilder.append(
- "(" + dateFormat.format(new Date(vcardFile.getLastModified())) + ")");
- stringBuilder.setSpan(
- new RelativeSizeSpan(0.7f), indexToBeSpanned, stringBuilder.length(),
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- items[i] = stringBuilder;
- }
- if (multipleSelect) {
- builder.setMultiChoiceItems(items, (boolean[])null, listener);
- } else {
- builder.setSingleChoiceItems(items, 0, listener);
- }
- return builder.create();
- }
-
- @Override
- protected void onCreate(Bundle bundle) {
- super.onCreate(bundle);
-
- 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);
- }
- } 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) {
- // There's three possibilities:
- // - more than one accounts -> ask the user
- // - just one account -> use the account without asking the user
- // - no account -> use phone-local storage without asking the user
- final Sources sources = Sources.getInstance(this);
- final List<Account> accountList = sources.getAccounts(true);
- final int size = accountList.size();
- if (size > 1) {
- final int resId = R.string.import_from_sdcard;
- mAccountSelectionListener =
- new AccountSelectionUtil.AccountSelectedListener(
- this, accountList, resId) {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- mAccount = mAccountList.get(which);
- // Instead of using Intent mechanism, call the relevant private method,
- // to avoid throwing an Intent to itself again.
- startImport();
- }
- };
- showDialog(resId);
- return;
- } else {
- mAccount = size > 0 ? accountList.get(0) : null;
- }
- }
-
- startImport();
- }
-
- private void startImport() {
- Intent intent = getIntent();
- 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);
- } else {
- doScanExternalStorageAndImportVCard();
- }
- }
-
- @Override
- protected Dialog onCreateDialog(int resId) {
- switch (resId) {
- case R.string.import_from_sdcard: {
- if (mAccountSelectionListener == null) {
- throw new NullPointerException(
- "mAccountSelectionListener must not be null.");
- }
- return AccountSelectionUtil.getSelectAccountDialog(this, resId,
- mAccountSelectionListener,
- new CancelListener());
- }
- case R.id.dialog_searching_vcard: {
- if (mProgressDialogForScanVCard == null) {
- String title = getString(R.string.searching_vcard_title);
- String message = getString(R.string.searching_vcard_message);
- mProgressDialogForScanVCard =
- ProgressDialog.show(this, title, message, true, false);
- mProgressDialogForScanVCard.setOnCancelListener(mVCardScanThread);
- mVCardScanThread.start();
- }
- return mProgressDialogForScanVCard;
- }
- case R.id.dialog_sdcard_not_found: {
- AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(R.string.no_sdcard_title)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(R.string.no_sdcard_message)
- .setOnCancelListener(mCancelListener)
- .setPositiveButton(android.R.string.ok, mCancelListener);
- return builder.create();
- }
- case R.id.dialog_vcard_not_found: {
- String message = (getString(R.string.scanning_sdcard_failed_message,
- getString(R.string.fail_reason_no_vcard_file)));
- AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(R.string.scanning_sdcard_failed_title)
- .setMessage(message)
- .setOnCancelListener(mCancelListener)
- .setPositiveButton(android.R.string.ok, mCancelListener);
- return builder.create();
- }
- case R.id.dialog_select_import_type: {
- return getSelectImportTypeDialog();
- }
- case R.id.dialog_select_multiple_vcard: {
- return getVCardFileSelectDialog(true);
- }
- 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)));
- AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(R.string.scanning_sdcard_failed_title)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(message)
- .setOnCancelListener(mCancelListener)
- .setPositiveButton(android.R.string.ok, mCancelListener);
- return builder.create();
- }
- case R.id.dialog_error_with_message: {
- String message = mErrorMessage;
- if (TextUtils.isEmpty(message)) {
- Log.e(LOG_TAG, "Error message is null while it must not.");
- message = getString(R.string.fail_reason_unknown);
- }
- AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(getString(R.string.reading_vcard_failed_title))
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(message)
- .setOnCancelListener(mCancelListener)
- .setPositiveButton(android.R.string.ok, mCancelListener);
- return builder.create();
- }
- }
-
- return super.onCreateDialog(resId);
- }
-
- @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
- // screen back to the caller Activity.
- if (!isFinishing()) {
- finish();
- }
- }
-
- @Override
- protected void onDestroy() {
- // The code assumes the handler runs on the UI thread. If not,
- // clearing the message queue is not enough, one would have to
- // 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);
- }
-
- mHandler = null; // Prevents memory leaks by breaking any circular dependency.
- super.onDestroy();
- }
-
- /**
- * Tries to run a given Runnable object when the UI thread can. Ignore it otherwise
- */
- private void runOnUIThread(Runnable runnable) {
- if (mHandler == null) {
- Log.w(LOG_TAG, "Handler object is null. No dialog is shown.");
- } else {
- mHandler.post(runnable);
- }
- }
-
- @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.
- * - When multiple vCard files are available, asks a user to select one.
- */
- private void doScanExternalStorageAndImportVCard() {
- // TODO: should use getExternalStorageState().
- final File file = Environment.getExternalStorageDirectory();
- if (!file.exists() || !file.isDirectory() || !file.canRead()) {
- showDialog(R.id.dialog_sdcard_not_found);
- } else {
- mVCardScanThread = new VCardScanThread(file);
- showDialog(R.id.dialog_searching_vcard);
- }
- }
-}
diff --git a/src/com/android/contacts/JoinContactActivity.java b/src/com/android/contacts/JoinContactActivity.java
new file mode 100644
index 0000000..0f3eab0
--- /dev/null
+++ b/src/com/android/contacts/JoinContactActivity.java
@@ -0,0 +1,92 @@
+/*
+ * 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.list.JoinContactListFragment;
+import com.android.contacts.list.OnContactPickerActionListener;
+
+import android.app.Activity;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+/**
+ * An activity that shows a list of contacts that can be joined with the target contact.
+ */
+public class JoinContactActivity extends Activity {
+
+ 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";
+
+ private long mTargetContactId;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+ 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();
+ return;
+ }
+
+ JoinContactListFragment fragment = new JoinContactListFragment();
+ fragment.setTargetContactId(mTargetContactId);
+ fragment.setOnContactPickerActionListener(new OnContactPickerActionListener() {
+ public void onPickContactAction(Uri contactUri) {
+ Intent intent = new Intent(null, contactUri);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+ public void onSearchAllContactsAction(String string) {
+ }
+
+ public void onShortcutIntentCreated(Intent intent) {
+ }
+
+ public void onCreateNewContactAction() {
+ }
+ });
+
+ FragmentTransaction transaction = openFragmentTransaction();
+ transaction.add(android.R.id.content, fragment);
+ transaction.commit();
+ }
+}
diff --git a/src/com/android/contacts/MultiplePhonePickerActivity.java b/src/com/android/contacts/MultiplePhonePickerActivity.java
new file mode 100644
index 0000000..08811e3
--- /dev/null
+++ b/src/com/android/contacts/MultiplePhonePickerActivity.java
@@ -0,0 +1,226 @@
+/*
+ * 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.list.MultiplePhonePickerFragment;
+import com.android.contacts.list.OnMultiplePhoneNumberPickerActionListener;
+
+import android.app.Activity;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Intents;
+import android.view.Menu;
+import android.view.MenuInflater;
+
+/**
+ * Displays of phone numbers and allows selection of multiple numbers.
+ */
+public class MultiplePhonePickerActivity extends Activity {
+
+ /**
+ * Display only selected recipients or not in MODE_PICK_MULTIPLE_PHONES mode
+ */
+ private boolean mShowSelectedOnly = false;
+
+ private MultiplePhonePickerFragment mListFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mListFragment = new MultiplePhonePickerFragment();
+ mListFragment.setOnMultiplePhoneNumberPickerActionListener(
+ new OnMultiplePhoneNumberPickerActionListener() {
+
+ public void onPhoneNumbersSelectedAction(Uri[] dataUris) {
+ returnActivityResult(dataUris);
+ }
+
+ public void onFinishAction() {
+ finish();
+ }
+ });
+
+ Parcelable[] extras = getIntent().getParcelableArrayExtra(Intents.EXTRA_PHONE_URIS);
+ mListFragment.setSelectedUris(extras);
+ FragmentTransaction transaction = openFragmentTransaction();
+ transaction.add(android.R.id.content, mListFragment);
+ transaction.commit();
+ }
+
+ @Override
+ public void onBackPressed() {
+ returnActivityResult(mListFragment.getSelectedUris());
+ super.onBackPressed();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle icicle) {
+ super.onSaveInstanceState(icicle);
+ mListFragment.onSaveInstanceState(icicle);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ final MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.pick, menu);
+ return true;
+ }
+
+ /*
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ 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;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ 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 super.onOptionsItemSelected(item);
+ }
+*/
+ @Override
+ public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
+ boolean globalSearch) {
+ // TODO
+// if (mProviderStatus != ProviderStatus.STATUS_NORMAL) {
+// return;
+// }
+//
+// if (globalSearch) {
+// super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+// } else {
+// if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0) {
+// if ((mMode & MODE_MASK_PICKER) != 0) {
+// Bundle extras = getIntent().getExtras();
+// if (extras == null) {
+// extras = new Bundle();
+// }
+// mUserSelection.fillSelectionForSearchMode(extras);
+// ContactsSearchManager.startSearchForResult(this, initialQuery,
+// SUBACTIVITY_FILTER, extras);
+// } else {
+// ContactsSearchManager.startSearch(this, initialQuery);
+// }
+// }
+// }
+ }
+
+// @Override
+// protected void startQuery(Uri uri, String[] projection) {
+// // 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));
+// } else {
+// mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
+// projection, CLAUSE_ONLY_VISIBLE, null, getSortOrder(projection));
+// }
+// }
+
+// @Override
+// public Cursor doFilter(String filter) {
+// String[] projection = getProjectionForQuery();
+// if (mSearchMode && TextUtils.isEmpty(mListFragment.getQueryString())) {
+// return new MatrixCursor(projection);
+// }
+//
+// final ContentResolver resolver = getContentResolver();
+// // Filter phone numbers as well.
+// mPhoneNumberAdapter.doFilter(filter, mShowSelectedOnly);
+//
+// Uri uri = getUriToQuery();
+// if (!TextUtils.isEmpty(filter)) {
+// uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(filter));
+// }
+// return resolver.query(uri, projection, CLAUSE_ONLY_VISIBLE, null, getSortOrder(projection));
+// }
+
+ public void returnActivityResult(Uri[] dataUris) {
+ Intent intent = new Intent();
+ intent.putExtra(Intents.EXTRA_PHONE_URIS, dataUris);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+ private void checkAll(boolean checked) {
+ // TODO fix this. It should iterate over the cursor rather than the views in the list.
+ /*
+ 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);
+ }
+ */
+ }
+}
diff --git a/src/com/android/contacts/PhoneDisambigDialog.java b/src/com/android/contacts/PhoneDisambigDialog.java
index d8cb14e..3b32b57 100644
--- a/src/com/android/contacts/PhoneDisambigDialog.java
+++ b/src/com/android/contacts/PhoneDisambigDialog.java
@@ -49,13 +49,12 @@
* one will be chosen to make a call or initiate an sms message.
*/
public class PhoneDisambigDialog implements DialogInterface.OnClickListener,
- DialogInterface.OnDismissListener, CompoundButton.OnCheckedChangeListener{
+ CompoundButton.OnCheckedChangeListener{
private boolean mMakePrimary = false;
private Context mContext;
private AlertDialog mDialog;
private boolean mSendSms;
- private Cursor mPhonesCursor;
private ListAdapter mPhonesAdapter;
private ArrayList<PhoneItem> mPhoneItemList;
@@ -66,9 +65,9 @@
public PhoneDisambigDialog(Context context, Cursor phonesCursor, boolean sendSms) {
mContext = context;
mSendSms = sendSms;
- mPhonesCursor = phonesCursor;
-
mPhoneItemList = makePhoneItemsList(phonesCursor);
+ phonesCursor.close();
+
Collapser.collapseList(mPhoneItemList);
mPhonesAdapter = new PhonesAdapter(mContext, mPhoneItemList, mSendSms);
@@ -90,6 +89,10 @@
mDialog = dialogBuilder.create();
}
+ public void setOnDismissListener(DialogInterface.OnDismissListener dismissListener) {
+ mDialog.setOnDismissListener(dismissListener);
+ }
+
/**
* Show the dialog.
*/
@@ -129,10 +132,6 @@
mMakePrimary = isChecked;
}
- public void onDismiss(DialogInterface dialog) {
- mPhonesCursor.close();
- }
-
private static class PhonesAdapter extends ArrayAdapter<PhoneItem> {
private final boolean sendSms;
private final Sources mSources;
diff --git a/src/com/android/contacts/PinnedHeaderListView.java b/src/com/android/contacts/PinnedHeaderListView.java
deleted file mode 100644
index 9d1391b..0000000
--- a/src/com/android/contacts/PinnedHeaderListView.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * 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.content.Context;
-import android.graphics.Canvas;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ListAdapter;
-import android.widget.ListView;
-
-/**
- * A ListView that maintains a header pinned at the top of the list. The
- * pinned header can be pushed up and dissolved as needed.
- */
-public class PinnedHeaderListView extends ListView {
-
- /**
- * Adapter interface. The list adapter must implement this interface.
- */
- public interface PinnedHeaderAdapter {
-
- /**
- * Pinned header state: don't show the header.
- */
- public static final int PINNED_HEADER_GONE = 0;
-
- /**
- * Pinned header state: show the header at the top of the list.
- */
- public static final int PINNED_HEADER_VISIBLE = 1;
-
- /**
- * Pinned header state: show the header. If the header extends beyond
- * the bottom of the first shown element, push it up and clip.
- */
- public static final int PINNED_HEADER_PUSHED_UP = 2;
-
- /**
- * Computes the desired state of the pinned header for the given
- * position of the first visible list item. Allowed return values are
- * {@link #PINNED_HEADER_GONE}, {@link #PINNED_HEADER_VISIBLE} or
- * {@link #PINNED_HEADER_PUSHED_UP}.
- */
- int getPinnedHeaderState(int position);
-
- /**
- * Configures the pinned header view to match the first visible list item.
- *
- * @param header pinned header view.
- * @param position position of the first visible list item.
- * @param alpha fading of the header view, between 0 and 255.
- */
- void configurePinnedHeader(View header, int position, int alpha);
- }
-
- private static final int MAX_ALPHA = 255;
-
- private PinnedHeaderAdapter mAdapter;
- private View mHeaderView;
- private boolean mHeaderViewVisible;
-
- private int mHeaderViewWidth;
-
- private int mHeaderViewHeight;
-
- public PinnedHeaderListView(Context context) {
- super(context);
- }
-
- public PinnedHeaderListView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- public void setPinnedHeaderView(View view) {
- mHeaderView = view;
-
- // Disable vertical fading when the pinned header is present
- // TODO change ListView to allow separate measures for top and bottom fading edge;
- // in this particular case we would like to disable the top, but not the bottom edge.
- if (mHeaderView != null) {
- setFadingEdgeLength(0);
- }
- requestLayout();
- }
-
- @Override
- public void setAdapter(ListAdapter adapter) {
- super.setAdapter(adapter);
- mAdapter = (PinnedHeaderAdapter)adapter;
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- if (mHeaderView != null) {
- measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);
- mHeaderViewWidth = mHeaderView.getMeasuredWidth();
- mHeaderViewHeight = mHeaderView.getMeasuredHeight();
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- if (mHeaderView != null) {
- mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
- configureHeaderView(getFirstVisiblePosition());
- }
- }
-
- public void configureHeaderView(int position) {
- if (mHeaderView == null) {
- return;
- }
-
- int state = mAdapter.getPinnedHeaderState(position);
- switch (state) {
- case PinnedHeaderAdapter.PINNED_HEADER_GONE: {
- mHeaderViewVisible = false;
- break;
- }
-
- case PinnedHeaderAdapter.PINNED_HEADER_VISIBLE: {
- mAdapter.configurePinnedHeader(mHeaderView, position, MAX_ALPHA);
- if (mHeaderView.getTop() != 0) {
- mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
- }
- mHeaderViewVisible = true;
- break;
- }
-
- case PinnedHeaderAdapter.PINNED_HEADER_PUSHED_UP: {
- View firstView = getChildAt(0);
- int bottom = firstView.getBottom();
- int itemHeight = firstView.getHeight();
- int headerHeight = mHeaderView.getHeight();
- int y;
- int alpha;
- if (bottom < headerHeight) {
- y = (bottom - headerHeight);
- alpha = MAX_ALPHA * (headerHeight + y) / headerHeight;
- } else {
- y = 0;
- alpha = MAX_ALPHA;
- }
- mAdapter.configurePinnedHeader(mHeaderView, position, alpha);
- if (mHeaderView.getTop() != y) {
- mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight + y);
- }
- mHeaderViewVisible = true;
- break;
- }
- }
- }
-
- @Override
- protected void dispatchDraw(Canvas canvas) {
- super.dispatchDraw(canvas);
- if (mHeaderViewVisible) {
- drawChild(canvas, mHeaderView, getDrawingTime());
- }
- }
-}
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/ScrollingTabWidget.java b/src/com/android/contacts/ScrollingTabWidget.java
deleted file mode 100644
index b45abe4..0000000
--- a/src/com/android/contacts/ScrollingTabWidget.java
+++ /dev/null
@@ -1,418 +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.content.Context;
-import android.graphics.Canvas;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.View.OnClickListener;
-import android.view.View.OnFocusChangeListener;
-import android.widget.HorizontalScrollView;
-import android.widget.ImageView;
-import android.widget.RelativeLayout;
-
-/*
- * Tab widget that can contain more tabs than can fit on screen at once and scroll over them.
- */
-public class ScrollingTabWidget extends RelativeLayout
- implements OnClickListener, ViewTreeObserver.OnGlobalFocusChangeListener,
- OnFocusChangeListener {
-
- private static final String TAG = "ScrollingTabWidget";
-
- private OnTabSelectionChangedListener mSelectionChangedListener;
- private int mSelectedTab = 0;
- private ImageView mLeftArrowView;
- private ImageView mRightArrowView;
- private HorizontalScrollView mTabsScrollWrapper;
- private TabStripView mTabsView;
- private LayoutInflater mInflater;
-
- // Keeps track of the left most visible tab.
- private int mLeftMostVisibleTabIndex = 0;
-
- public ScrollingTabWidget(Context context) {
- this(context, null);
- }
-
- public ScrollingTabWidget(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ScrollingTabWidget(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs);
-
- mInflater = (LayoutInflater) mContext.getSystemService(
- Context.LAYOUT_INFLATER_SERVICE);
-
- setFocusable(true);
- setOnFocusChangeListener(this);
- if (!hasFocus()) {
- setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
- }
-
- mLeftArrowView = (ImageView) mInflater.inflate(R.layout.tab_left_arrow, this, false);
- mLeftArrowView.setOnClickListener(this);
- mRightArrowView = (ImageView) mInflater.inflate(R.layout.tab_right_arrow, this, false);
- mRightArrowView.setOnClickListener(this);
- mTabsScrollWrapper = (HorizontalScrollView) mInflater.inflate(
- R.layout.tab_layout, this, false);
- mTabsView = (TabStripView) mTabsScrollWrapper.findViewById(android.R.id.tabs);
- View accountNameView = mInflater.inflate(R.layout.tab_account_name, this, false);
-
- mLeftArrowView.setVisibility(View.INVISIBLE);
- mRightArrowView.setVisibility(View.INVISIBLE);
-
- addView(mTabsScrollWrapper);
- addView(mLeftArrowView);
- addView(mRightArrowView);
- addView(accountNameView);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- final ViewTreeObserver treeObserver = getViewTreeObserver();
- if (treeObserver != null) {
- treeObserver.addOnGlobalFocusChangeListener(this);
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- final ViewTreeObserver treeObserver = getViewTreeObserver();
- if (treeObserver != null) {
- treeObserver.removeOnGlobalFocusChangeListener(this);
- }
- }
-
- protected void updateArrowVisibility() {
- int scrollViewLeftEdge = mTabsScrollWrapper.getScrollX();
- int tabsViewLeftEdge = mTabsView.getLeft();
- int scrollViewRightEdge = scrollViewLeftEdge + mTabsScrollWrapper.getWidth();
- int tabsViewRightEdge = mTabsView.getRight();
-
- int rightArrowCurrentVisibility = mRightArrowView.getVisibility();
- if (scrollViewRightEdge == tabsViewRightEdge
- && rightArrowCurrentVisibility == View.VISIBLE) {
- mRightArrowView.setVisibility(View.INVISIBLE);
- } else if (scrollViewRightEdge < tabsViewRightEdge
- && rightArrowCurrentVisibility != View.VISIBLE) {
- mRightArrowView.setVisibility(View.VISIBLE);
- }
-
- int leftArrowCurrentVisibility = mLeftArrowView.getVisibility();
- if (scrollViewLeftEdge == tabsViewLeftEdge
- && leftArrowCurrentVisibility == View.VISIBLE) {
- mLeftArrowView.setVisibility(View.INVISIBLE);
- } else if (scrollViewLeftEdge > tabsViewLeftEdge
- && leftArrowCurrentVisibility != View.VISIBLE) {
- mLeftArrowView.setVisibility(View.VISIBLE);
- }
- }
-
- /**
- * Returns the tab indicator view at the given index.
- *
- * @param index the zero-based index of the tab indicator view to return
- * @return the tab indicator view at the given index
- */
- public View getChildTabViewAt(int index) {
- return mTabsView.getChildAt(index);
- }
-
- /**
- * Returns the number of tab indicator views.
- *
- * @return the number of tab indicator views.
- */
- public int getTabCount() {
- return mTabsView.getChildCount();
- }
-
- /**
- * Returns the {@link ViewGroup} that actually contains the tabs. This is where the tab
- * views should be attached to when being inflated.
- */
- public ViewGroup getTabParent() {
- return mTabsView;
- }
-
- public void removeAllTabs() {
- mTabsView.removeAllViews();
- }
-
- @Override
- public void dispatchDraw(Canvas canvas) {
- updateArrowVisibility();
- super.dispatchDraw(canvas);
- }
-
- /**
- * Sets the current tab.
- * This method is used to bring a tab to the front of the Widget,
- * and is used to post to the rest of the UI that a different tab
- * has been brought to the foreground.
- *
- * Note, this is separate from the traditional "focus" that is
- * employed from the view logic.
- *
- * For instance, if we have a list in a tabbed view, a user may be
- * navigating up and down the list, moving the UI focus (orange
- * highlighting) through the list items. The cursor movement does
- * not effect the "selected" tab though, because what is being
- * scrolled through is all on the same tab. The selected tab only
- * changes when we navigate between tabs (moving from the list view
- * to the next tabbed view, in this example).
- *
- * To move both the focus AND the selected tab at once, please use
- * {@link #focusCurrentTab}. Normally, the view logic takes care of
- * adjusting the focus, so unless you're circumventing the UI,
- * you'll probably just focus your interest here.
- *
- * @param index The tab that you want to indicate as the selected
- * tab (tab brought to the front of the widget)
- *
- * @see #focusCurrentTab
- */
- public void setCurrentTab(int index) {
- if (index < 0 || index >= getTabCount()) {
- return;
- }
-
- if (mSelectedTab < getTabCount()) {
- mTabsView.setSelected(mSelectedTab, false);
- }
- mSelectedTab = index;
- mTabsView.setSelected(mSelectedTab, true);
- }
-
- /**
- * Return index of the currently selected tab.
- */
- public int getCurrentTab() {
- return mSelectedTab;
- }
-
- /**
- * Sets the current tab and focuses the UI on it.
- * This method makes sure that the focused tab matches the selected
- * tab, normally at {@link #setCurrentTab}. Normally this would not
- * be an issue if we go through the UI, since the UI is responsible
- * for calling TabWidget.onFocusChanged(), but in the case where we
- * are selecting the tab programmatically, we'll need to make sure
- * focus keeps up.
- *
- * @param index The tab that you want focused (highlighted in orange)
- * and selected (tab brought to the front of the widget)
- *
- * @see #setCurrentTab
- */
- public void focusCurrentTab(int index) {
- if (index < 0 || index >= getTabCount()) {
- return;
- }
-
- setCurrentTab(index);
- getChildTabViewAt(index).requestFocus();
-
- }
-
- /**
- * Adds a tab to the list of tabs. The tab's indicator view is specified
- * by a layout id. InflateException will be thrown if there is a problem
- * inflating.
- *
- * @param layoutResId The layout id to be inflated to make the tab indicator.
- */
- public void addTab(int layoutResId) {
- addTab(mInflater.inflate(layoutResId, mTabsView, false));
- }
-
- /**
- * Adds a tab to the list of tabs. The tab's indicator view must be provided.
- *
- * @param child
- */
- public void addTab(View child) {
- if (child == null) {
- return;
- }
-
- if (child.getLayoutParams() == null) {
- final LayoutParams lp = new LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- lp.setMargins(0, 0, 0, 0);
- child.setLayoutParams(lp);
- }
-
- // Ensure you can navigate to the tab with the keyboard, and you can touch it
- child.setFocusable(true);
- child.setClickable(true);
- child.setOnClickListener(new TabClickListener());
- child.setOnFocusChangeListener(this);
-
- mTabsView.addView(child);
- }
-
- /**
- * Provides a way for ViewContactActivity and EditContactActivity to be notified that the
- * user clicked on a tab indicator.
- */
- public void setTabSelectionListener(OnTabSelectionChangedListener listener) {
- mSelectionChangedListener = listener;
- }
-
- public void onGlobalFocusChanged(View oldFocus, View newFocus) {
- if (isTab(oldFocus) && !isTab(newFocus)) {
- onLoseFocus();
- }
- }
-
- public void onFocusChange(View v, boolean hasFocus) {
- if (v == this && hasFocus) {
- onObtainFocus();
- return;
- }
-
- if (hasFocus) {
- for (int i = 0; i < getTabCount(); i++) {
- if (getChildTabViewAt(i) == v) {
- setCurrentTab(i);
- mSelectionChangedListener.onTabSelectionChanged(i, false);
- break;
- }
- }
- }
- }
-
- /**
- * Called when the {@link ScrollingTabWidget} gets focus. Here the
- * widget decides which of it's tabs should have focus.
- */
- protected void onObtainFocus() {
- // Setting this flag, allows the children of this View to obtain focus.
- setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
- // Assign focus to the last selected tab.
- focusCurrentTab(mSelectedTab);
- mSelectionChangedListener.onTabSelectionChanged(mSelectedTab, false);
- }
-
- /**
- * Called when the focus has left the {@link ScrollingTabWidget} or its
- * descendants. At this time we want the children of this view to be marked
- * as un-focusable, so that next time focus is moved to the widget, the widget
- * gets control, and can assign focus where it wants.
- */
- protected void onLoseFocus() {
- // Setting this flag will effectively make the tabs unfocusable. This will
- // be toggled when the widget obtains focus again.
- setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
- }
-
- public boolean isTab(View v) {
- for (int i = 0; i < getTabCount(); i++) {
- if (getChildTabViewAt(i) == v) {
- return true;
- }
- }
- return false;
- }
-
- private class TabClickListener implements OnClickListener {
- public void onClick(View v) {
- for (int i = 0; i < getTabCount(); i++) {
- if (getChildTabViewAt(i) == v) {
- setCurrentTab(i);
- mSelectionChangedListener.onTabSelectionChanged(i, true);
- break;
- }
- }
- }
- }
-
- public interface OnTabSelectionChangedListener {
- /**
- * Informs the tab widget host which tab was selected. It also indicates
- * if the tab was clicked/pressed or just focused into.
- *
- * @param tabIndex index of the tab that was selected
- * @param clicked whether the selection changed due to a touch/click
- * or due to focus entering the tab through navigation. Pass true
- * if it was due to a press/click and false otherwise.
- */
- void onTabSelectionChanged(int tabIndex, boolean clicked);
- }
-
- public void onClick(View v) {
- updateLeftMostVisible();
- if (v == mRightArrowView && (mLeftMostVisibleTabIndex + 1 < getTabCount())) {
- tabScroll(true /* right */);
- } else if (v == mLeftArrowView && mLeftMostVisibleTabIndex > 0) {
- tabScroll(false /* left */);
- }
- }
-
- /*
- * Updates our record of the left most visible tab. We keep track of this explicitly
- * on arrow clicks, but need to re-calibrate after focus navigation.
- */
- protected void updateLeftMostVisible() {
- int viewableLeftEdge = mTabsScrollWrapper.getScrollX();
-
- if (mLeftArrowView.getVisibility() == View.VISIBLE) {
- viewableLeftEdge += mLeftArrowView.getWidth();
- }
-
- for (int i = 0; i < getTabCount(); i++) {
- View tab = getChildTabViewAt(i);
- int tabLeftEdge = tab.getLeft();
- if (tabLeftEdge >= viewableLeftEdge) {
- mLeftMostVisibleTabIndex = i;
- break;
- }
- }
- }
-
- /**
- * Scrolls the tabs by exactly one tab width.
- *
- * @param directionRight if true, scroll to the right, if false, scroll to the left.
- */
- protected void tabScroll(boolean directionRight) {
- int scrollWidth = 0;
- View newLeftMostVisibleTab = null;
- if (directionRight) {
- newLeftMostVisibleTab = getChildTabViewAt(++mLeftMostVisibleTabIndex);
- } else {
- newLeftMostVisibleTab = getChildTabViewAt(--mLeftMostVisibleTabIndex);
- }
-
- scrollWidth = newLeftMostVisibleTab.getLeft() - mTabsScrollWrapper.getScrollX();
- if (mLeftMostVisibleTabIndex > 0) {
- scrollWidth -= mLeftArrowView.getWidth();
- }
- mTabsScrollWrapper.smoothScrollBy(scrollWidth, 0);
- }
-
-}
diff --git a/src/com/android/contacts/SearchEditText.java b/src/com/android/contacts/SearchEditText.java
deleted file mode 100644
index 7683f23..0000000
--- a/src/com/android/contacts/SearchEditText.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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.content.Context;
-import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.widget.EditText;
-
-/**
- * A custom text editor that helps automatically dismiss the activity along with the soft
- * keyboard.
- */
-public class SearchEditText extends EditText {
-
- private boolean mMagnifyingGlassShown = true;
- private Drawable mMagnifyingGlass;
-
- public SearchEditText(Context context, AttributeSet attrs) {
- super(context, attrs);
- mMagnifyingGlass = getCompoundDrawables()[2];
- }
-
- /**
- * Conditionally shows a magnifying glass icon on the right side of the text field
- * when the text it empty.
- */
- @Override
- public boolean onPreDraw() {
- boolean emptyText = TextUtils.isEmpty(getText());
- if (mMagnifyingGlassShown != emptyText) {
- mMagnifyingGlassShown = emptyText;
- if (mMagnifyingGlassShown) {
- setCompoundDrawables(null, null, mMagnifyingGlass, null);
- } else {
- setCompoundDrawables(null, null, null, null);
- }
- return false;
- }
- return super.onPreDraw();
- }
-
- /**
- * Forwards the onKeyPreIme call to the view's activity.
- */
- @Override
- public boolean onKeyPreIme(int keyCode, KeyEvent event) {
- if (((ContactsListActivity)getContext()).onKeyPreIme(keyCode, event)) {
- return true;
- }
- return super.onKeyPreIme(keyCode, event);
- }
-}
diff --git a/src/com/android/contacts/TwelveKeyDialer.java b/src/com/android/contacts/TwelveKeyDialer.java
index 07927de..bb4f496 100644
--- a/src/com/android/contacts/TwelveKeyDialer.java
+++ b/src/com/android/contacts/TwelveKeyDialer.java
@@ -606,25 +606,6 @@
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_CALL: {
- // TODO: In dialButtonPressed we do some of these
- // tests again. We should try to consolidate them in
- // one place.
- if (!phoneIsCdma() && mIsAddCallMode && isDigitsEmpty()) {
- // For CDMA phones, we always call
- // dialButtonPressed() because we may need to send
- // an empty flash command to the network.
- // Otherwise, if we are adding a call from the
- // InCallScreen and the phone number entered is
- // empty, we just close the dialer to expose the
- // InCallScreen under it.
- finish();
- }
-
- // If we're CDMA, regardless of where we are adding a call from (either
- // InCallScreen or Dialtacts), the user may need to send an empty
- // flash command to the network. So let's call dialButtonPressed() regardless
- // and dialButtonPressed will handle this functionality for us.
- // otherwise, we place the call.
dialButtonPressed();
return true;
}
@@ -763,52 +744,55 @@
}
void callVoicemail() {
- Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
- Uri.fromParts("voicemail", EMPTY_NUMBER, null));
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- mDigits.getText().clear();
+ startActivity(newVoicemailIntent());
+ mDigits.getText().clear(); // TODO: Fix bug 1745781
finish();
}
+ /**
+ * In most cases, when the dial button is pressed, there is a
+ * number in digits area. Pack it in the intent, start the
+ * outgoing call broadcast as a separate task and finish this
+ * activity.
+ *
+ * When there is no digit and the phone is CDMA and off hook,
+ * we're sending a blank flash for CDMA. CDMA networks use Flash
+ * messages when special processing needs to be done, mainly for
+ * 3-way or call waiting scenarios. Presumably, here we're in a
+ * special 3-way scenario where the network needs a blank flash
+ * before being able to add the new participant. (This is not the
+ * case with all 3-way calls, just certain CDMA infrastructures.)
+ *
+ * Otherwise, there is no digit, display the last dialed
+ * number. Don't finish since the user may want to edit it. The
+ * user needs to press the dial button again, to dial it (general
+ * case described above).
+ */
void dialButtonPressed() {
- final String number = mDigits.getText().toString();
- boolean sendEmptyFlash = false;
- Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED);
-
- if (isDigitsEmpty()) { // There is no number entered.
+ if (isDigitsEmpty()) { // No number entered.
if (phoneIsCdma() && phoneIsOffhook()) {
- // On CDMA phones, if we're already on a call, pressing
- // the Dial button without entering any digits means "send
- // an empty flash."
- intent.setData(Uri.fromParts("tel", EMPTY_NUMBER, null));
- intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true);
- sendEmptyFlash = true;
- } else if (!TextUtils.isEmpty(mLastNumberDialed)) {
- // Otherwise, pressing the Dial button without entering
- // any digits means "recall the last number dialed".
- mDigits.setText(mLastNumberDialed);
- return;
+ // This is really CDMA specific. On GSM is it possible
+ // to be off hook and wanted to add a 3rd party using
+ // the redial feature.
+ startActivity(newFlashIntent());
} else {
- // Rare case: there's no "last number dialed". There's
- // nothing useful for the Dial button to do in this case.
- playTone(ToneGenerator.TONE_PROP_NACK);
- return;
+ if (!TextUtils.isEmpty(mLastNumberDialed)) {
+ mDigits.setText(mLastNumberDialed);
+ } else {
+ // There's no "last number dialed" or the
+ // background query is still running. There's
+ // nothing useful for the Dial button to do in
+ // this case. Note: with a soft dial button, this
+ // can never happens since the dial button is
+ // disabled under these conditons.
+ playTone(ToneGenerator.TONE_PROP_NACK);
+ }
}
- } else { // There is a number.
- intent.setData(Uri.fromParts("tel", number, null));
- }
+ } else {
+ final String number = mDigits.getText().toString();
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- mDigits.getText().clear();
-
- // Don't finish TwelveKeyDialer yet if we're sending a blank flash for CDMA. CDMA
- // networks use Flash messages when special processing needs to be done, mainly for
- // 3-way or call waiting scenarios. Presumably, here we're in a special 3-way scenario
- // where the network needs a blank flash before being able to add the new participant.
- // (This is not the case with all 3-way calls, just certain CDMA infrastructures.)
- if (!sendEmptyFlash) {
+ startActivity(newDialNumberIntent(number));
+ mDigits.getText().clear(); // TODO: Fix bug 1745781
finish();
}
}
@@ -1256,4 +1240,25 @@
ContactsSearchManager.startSearch(this, initialQuery);
}
}
+
+ // Helpers for the call intents.
+ private Intent newVoicemailIntent() {
+ final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ Uri.fromParts("voicemail", EMPTY_NUMBER, null));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ return intent;
+ }
+
+ private Intent newFlashIntent() {
+ final Intent intent = newDialNumberIntent(EMPTY_NUMBER);
+ intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true);
+ return intent;
+ }
+
+ private Intent newDialNumberIntent(String number) {
+ final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ Uri.fromParts("tel", number, null));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ return intent;
+ }
}
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
deleted file mode 100644
index ead6a4a..0000000
--- a/src/com/android/contacts/ViewContactActivity.java
+++ /dev/null
@@ -1,1363 +0,0 @@
-/*
- * 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 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.ui.EditContactActivity;
-import com.android.contacts.util.Constants;
-import com.android.contacts.util.DataStatus;
-import com.android.contacts.util.NotifyingAsyncQueryHandler;
-import com.android.internal.telephony.ITelephony;
-import com.android.internal.widget.ContactHeaderWidget;
-import com.google.android.collect.Lists;
-import com.google.android.collect.Maps;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.content.ActivityNotFoundException;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Entity;
-import android.content.EntityIterator;
-import android.content.Intent;
-import android.content.Entity.NamedContentValues;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.graphics.drawable.Drawable;
-import android.net.ParseException;
-import android.net.Uri;
-import android.net.WebAddress;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.AggregationExceptions;
-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.RawContactsEntity;
-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.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.Window;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.widget.AdapterView;
-import android.widget.ImageView;
-import android.widget.ListView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
-/**
- * Displays the details of a specific contact.
- */
-public class ViewContactActivity extends Activity
- implements View.OnCreateContextMenuListener, DialogInterface.OnClickListener,
- AdapterView.OnItemClickListener, NotifyingAsyncQueryHandler.AsyncQueryListener {
- private static final String TAG = "ViewContact";
-
- private static final boolean SHOW_SEPARATORS = false;
-
- 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 REQUEST_JOIN_CONTACT = 1;
- private static final int REQUEST_EDIT_CONTACT = 2;
-
- public static final int MENU_ITEM_MAKE_DEFAULT = 3;
-
- protected Uri mLookupUri;
- private ContentResolver mResolver;
- private ViewAdapter mAdapter;
- private int mNumPhoneNumbers = 0;
-
- /**
- * A list of distinct contact IDs included in the current contact.
- */
- private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
-
- /* package */ ArrayList<ViewEntry> mPhoneEntries = new ArrayList<ViewEntry>();
- /* package */ ArrayList<ViewEntry> mSmsEntries = new ArrayList<ViewEntry>();
- /* package */ ArrayList<ViewEntry> mEmailEntries = new ArrayList<ViewEntry>();
- /* package */ ArrayList<ViewEntry> mPostalEntries = new ArrayList<ViewEntry>();
- /* package */ ArrayList<ViewEntry> mImEntries = new ArrayList<ViewEntry>();
- /* package */ ArrayList<ViewEntry> mNicknameEntries = new ArrayList<ViewEntry>();
- /* package */ ArrayList<ViewEntry> mOrganizationEntries = new ArrayList<ViewEntry>();
- /* package */ ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>();
- /* package */ ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
- /* package */ ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
-
- private Cursor mCursor;
-
- protected ContactHeaderWidget mContactHeaderWidget;
- private NotifyingAsyncQueryHandler mHandler;
-
- protected LayoutInflater mInflater;
-
- protected int mReadOnlySourcesCnt;
- protected int mWritableSourcesCnt;
- protected boolean mAllRestricted;
-
- protected Uri mPrimaryPhoneUri = null;
-
- protected ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
-
- private static final int TOKEN_ENTITIES = 0;
- private static final int TOKEN_STATUSES = 1;
-
- private boolean mHasEntities = false;
- private boolean mHasStatuses = false;
-
- private long mNameRawContactId = -1;
- private int mDisplayNameSource = DisplayNameSources.UNDEFINED;
-
- private ArrayList<Entity> mEntities = Lists.newArrayList();
- private HashMap<Long, DataStatus> mStatuses = Maps.newHashMap();
-
- /**
- * 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;
-
- private ContentObserver mObserver = new ContentObserver(new Handler()) {
- @Override
- public boolean deliverSelfNotifications() {
- return true;
- }
-
- @Override
- public void onChange(boolean selfChange) {
- if (mCursor != null && !mCursor.isClosed()) {
- startEntityQuery();
- }
- }
- };
-
- public void onClick(DialogInterface dialog, int which) {
- closeCursor();
- getContentResolver().delete(mLookupUri, null, null);
- finish();
- }
-
- private ListView mListView;
- private boolean mShowSmsLinksForAllPhones;
-
- @Override
- protected void onCreate(Bundle icicle) {
- super.onCreate(icicle);
-
- final Intent intent = getIntent();
- Uri data = intent.getData();
- String authority = data.getAuthority();
- if (ContactsContract.AUTHORITY.equals(authority)) {
- mLookupUri = data;
- } else if (android.provider.Contacts.AUTHORITY.equals(authority)) {
- final long rawContactId = ContentUris.parseId(data);
- mLookupUri = RawContacts.getContactLookupUri(getContentResolver(),
- ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
-
- }
- mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
- requestWindowFeature(Window.FEATURE_NO_TITLE);
- setContentView(R.layout.contact_card_layout);
-
- mContactHeaderWidget = (ContactHeaderWidget) findViewById(R.id.contact_header_widget);
- mContactHeaderWidget.showStar(true);
- mContactHeaderWidget.setExcludeMimes(new String[] {
- Contacts.CONTENT_ITEM_TYPE
- });
-
- mHandler = new NotifyingAsyncQueryHandler(this, this);
-
- mListView = (ListView) findViewById(R.id.contact_data);
- 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);
-
- mResolver = getContentResolver();
-
- // 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;
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- startEntityQuery();
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- closeCursor();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- closeCursor();
- }
-
- @Override
- protected Dialog onCreateDialog(int id) {
- switch (id) {
- case DIALOG_CONFIRM_DELETE:
- return new AlertDialog.Builder(this)
- .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, this)
- .setCancelable(false)
- .create();
- case DIALOG_CONFIRM_READONLY_DELETE:
- return new AlertDialog.Builder(this)
- .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, this)
- .setCancelable(false)
- .create();
- case DIALOG_CONFIRM_MULTIPLE_DELETE:
- return new AlertDialog.Builder(this)
- .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, this)
- .setCancelable(false)
- .create();
- case DIALOG_CONFIRM_READONLY_HIDE: {
- return new AlertDialog.Builder(this)
- .setTitle(R.string.deleteConfirmation_title)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(R.string.readOnlyContactWarning)
- .setPositiveButton(android.R.string.ok, this)
- .create();
- }
-
- }
- return null;
- }
-
- /** {@inheritDoc} */
- public void onQueryComplete(int token, Object cookie, final Cursor cursor) {
- if (token == TOKEN_STATUSES) {
- try {
- // Read available social rows and consider binding
- readStatuses(cursor);
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- considerBindData();
- return;
- }
-
- // One would think we could just iterate over the Cursor
- // directly here, as the result set should be small, and we've
- // already run the query in an AsyncTask, but a lot of ANRs
- // were being reported in this code nonetheless. See bug
- // 2539603 for details. The real bug which makes this result
- // set huge and CPU-heavy may be elsewhere.
- // TODO: if we keep this async, perhaps the entity iteration
- // should also be original AsyncTask, rather than ping-ponging
- // between threads like this.
- final ArrayList<Entity> oldEntities = mEntities;
- (new AsyncTask<Void, Void, ArrayList<Entity>>() {
- @Override
- protected ArrayList<Entity> doInBackground(Void... params) {
- ArrayList<Entity> newEntities = new ArrayList<Entity>(cursor.getCount());
- EntityIterator iterator = RawContacts.newEntityIterator(cursor);
- try {
- while (iterator.hasNext()) {
- Entity entity = iterator.next();
- newEntities.add(entity);
- }
- } finally {
- iterator.close();
- }
- return newEntities;
- }
-
- @Override
- protected void onPostExecute(ArrayList<Entity> newEntities) {
- if (newEntities == null) {
- // There was an error loading.
- return;
- }
- synchronized (ViewContactActivity.this) {
- if (mEntities != oldEntities) {
- // Multiple async tasks were in flight and we
- // lost the race.
- return;
- }
- mEntities = newEntities;
- mHasEntities = true;
- }
- considerBindData();
- }
- }).execute();
- }
-
- private long getRefreshedContactId() {
- Uri freshContactUri = Contacts.lookupContact(getContentResolver(), mLookupUri);
- if (freshContactUri != null) {
- return ContentUris.parseId(freshContactUri);
- }
- return -1;
- }
-
- /**
- * Read from the given {@link Cursor} and build a set of {@link DataStatus}
- * objects to match any valid statuses found.
- */
- private synchronized void readStatuses(Cursor cursor) {
- mStatuses.clear();
-
- // Walk found statuses, creating internal row for each
- while (cursor.moveToNext()) {
- final DataStatus status = new DataStatus(cursor);
- final long dataId = cursor.getLong(StatusQuery._ID);
- mStatuses.put(dataId, status);
- }
-
- mHasStatuses = true;
- }
-
- private static Cursor setupContactCursor(ContentResolver resolver, Uri lookupUri) {
- if (lookupUri == null) {
- return null;
- }
- final List<String> segments = lookupUri.getPathSegments();
- if (segments.size() != 4) {
- return null;
- }
-
- // Contains an Id.
- final long uriContactId = Long.parseLong(segments.get(3));
- final String uriLookupKey = Uri.encode(segments.get(2));
- final Uri dataUri = Uri.withAppendedPath(
- ContentUris.withAppendedId(Contacts.CONTENT_URI, uriContactId),
- Contacts.Data.CONTENT_DIRECTORY);
-
- // This cursor has several purposes:
- // - Fetch NAME_RAW_CONTACT_ID and DISPLAY_NAME_SOURCE
- // - Fetch the lookup-key to ensure we are looking at the right record
- // - Watcher for change events
- Cursor cursor = resolver.query(dataUri,
- new String[] {
- Contacts.NAME_RAW_CONTACT_ID,
- Contacts.DISPLAY_NAME_SOURCE,
- Contacts.LOOKUP_KEY
- }, null, null, null);
-
- if (cursor.moveToFirst()) {
- String lookupKey =
- cursor.getString(cursor.getColumnIndex(Contacts.LOOKUP_KEY));
- if (!lookupKey.equals(uriLookupKey)) {
- // ID and lookup key do not match
- cursor.close();
- return null;
- }
- return cursor;
- } else {
- cursor.close();
- return null;
- }
- }
-
- private synchronized void startEntityQuery() {
- closeCursor();
-
- // Interprete mLookupUri
- mCursor = setupContactCursor(mResolver, mLookupUri);
-
- // If mCursor is null now we did not succeed in using the Uri's Id (or it didn't contain
- // a Uri). Instead we now have to use the lookup key to find the record
- if (mCursor == null) {
- mLookupUri = Contacts.getLookupUri(getContentResolver(), mLookupUri);
- mCursor = setupContactCursor(mResolver, mLookupUri);
- }
-
- // If mCursor is still null, we were unsuccessful in finding the record
- if (mCursor == null) {
- mNameRawContactId = -1;
- mDisplayNameSource = DisplayNameSources.UNDEFINED;
- // TODO either figure out a way to prevent a flash of black background or
- // use some other UI than a toast
- Toast.makeText(this, R.string.invalidContactMessage, Toast.LENGTH_SHORT).show();
- Log.e(TAG, "invalid contact uri: " + mLookupUri);
- finish();
- return;
- }
-
- final long contactId = ContentUris.parseId(mLookupUri);
-
- mNameRawContactId =
- mCursor.getLong(mCursor.getColumnIndex(Contacts.NAME_RAW_CONTACT_ID));
- mDisplayNameSource =
- mCursor.getInt(mCursor.getColumnIndex(Contacts.DISPLAY_NAME_SOURCE));
-
- mCursor.registerContentObserver(mObserver);
-
- // Clear flags and start queries to data and status
- mHasEntities = false;
- mHasStatuses = false;
-
- mHandler.startQuery(TOKEN_ENTITIES, null, RawContactsEntity.CONTENT_URI, null,
- RawContacts.CONTACT_ID + "=?", new String[] {
- String.valueOf(contactId)
- }, null);
- final Uri dataUri = Uri.withAppendedPath(
- ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
- Contacts.Data.CONTENT_DIRECTORY);
- mHandler.startQuery(TOKEN_STATUSES, null, dataUri, StatusQuery.PROJECTION,
- StatusUpdates.PRESENCE + " IS NOT NULL OR " + StatusUpdates.STATUS
- + " IS NOT NULL", null, null);
-
- mContactHeaderWidget.bindFromContactLookupUri(mLookupUri);
- }
-
- private void closeCursor() {
- if (mCursor != null) {
- mCursor.unregisterContentObserver(mObserver);
- mCursor.close();
- mCursor = null;
- }
- }
-
- /**
- * Consider binding views after any of several background queries has
- * completed. We check internal flags and only bind when all data has
- * arrived.
- */
- private void considerBindData() {
- if (mHasEntities && mHasStatuses) {
- 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(this, mSections);
- mListView.setAdapter(mAdapter);
- } else {
- mAdapter.setSections(mSections, SHOW_SEPARATORS);
- }
- mListView.setEmptyView(mEmptyView);
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- super.onCreateOptionsMenu(menu);
-
- final MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.view, menu);
- return true;
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- super.onPrepareOptionsMenu(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;
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View view, 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);
- }
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_edit: {
- Long rawContactIdToEdit = null;
- if (mRawContactIds.size() > 0) {
- rawContactIdToEdit = mRawContactIds.get(0);
- } else {
- // There is no rawContact to edit.
- break;
- }
- Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
- rawContactIdToEdit);
- startActivityForResult(new Intent(Intent.ACTION_EDIT, rawContactUri),
- REQUEST_EDIT_CONTACT);
- break;
- }
- case R.id.menu_delete: {
- // Get confirmation
- 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);
- }
- return true;
- }
- case R.id.menu_join: {
- showJoinAggregateActivity();
- return true;
- }
- case R.id.menu_options: {
- showOptionsActivity();
- return true;
- }
- case R.id.menu_share: {
- if (mAllRestricted) return false;
-
- // TODO: Keep around actual LOOKUP_KEY, or formalize method of extracting
- final String lookupKey = Uri.encode(mLookupUri.getPathSegments().get(2));
- 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 = getText(R.string.share_via);
- final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
-
- try {
- startActivity(chooseIntent);
- } catch (ActivityNotFoundException ex) {
- Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
- }
- return true;
- }
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case MENU_ITEM_MAKE_DEFAULT: {
- if (makeItemDefault(item)) {
- return true;
- }
- break;
- }
- }
-
- return super.onContextItemSelected(item);
- }
-
- 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);
- getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, entry.id),
- values, null, null);
- startEntityQuery();
- return true;
- }
-
- /**
- * Shows a list of aggregates that can be joined into the currently viewed aggregate.
- */
- public void showJoinAggregateActivity() {
- long freshId = getRefreshedContactId();
- if (freshId > 0) {
- String displayName = null;
- 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);
- }
- startActivityForResult(intent, REQUEST_JOIN_CONTACT);
- }
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
- if (requestCode == REQUEST_JOIN_CONTACT) {
- if (resultCode == RESULT_OK && intent != null) {
- final long contactId = ContentUris.parseId(intent.getData());
- joinAggregate(contactId);
- }
- } else if (requestCode == REQUEST_EDIT_CONTACT) {
- if (resultCode == EditContactActivity.RESULT_CLOSE_VIEW_ACTIVITY) {
- finish();
- } else if (resultCode == Activity.RESULT_OK) {
- mLookupUri = intent.getData();
- if (mLookupUri == null) {
- finish();
- }
- }
- }
- }
-
- private void joinAggregate(final long contactId) {
- Cursor c = mResolver.query(RawContacts.CONTENT_URI, new String[] {RawContacts._ID},
- RawContacts.CONTACT_ID + "=" + contactId, null, null);
-
- try {
- while(c.moveToNext()) {
- long rawContactId = c.getLong(0);
- setAggregationException(rawContactId, AggregationExceptions.TYPE_KEEP_TOGETHER);
- }
- } finally {
- c.close();
- }
-
- Toast.makeText(this, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show();
- startEntityQuery();
- }
-
- /**
- * Given a contact ID sets an aggregation exception to either join the contact with the
- * current aggregate or split off.
- */
- protected void setAggregationException(long rawContactId, int exceptionType) {
- ContentValues values = new ContentValues(3);
- for (long aRawContactId : mRawContactIds) {
- if (aRawContactId != rawContactId) {
- values.put(AggregationExceptions.RAW_CONTACT_ID1, aRawContactId);
- values.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId);
- values.put(AggregationExceptions.TYPE, exceptionType);
- mResolver.update(AggregationExceptions.CONTENT_URI, values, null, null);
- }
- }
- }
-
- private void showOptionsActivity() {
- final Intent intent = new Intent(this, ContactOptionsActivity.class);
- intent.setData(mLookupUri);
- startActivity(intent);
- }
-
- 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) {
- ViewEntry entry = ViewAdapter.getEntry(mSections, index, SHOW_SEPARATORS);
- if (entry != null &&
- entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) {
- 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);
- startActivity(intent);
- return true;
- }
- return false;
- }
-
- case KeyEvent.KEYCODE_DEL: {
- 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);
- }
- return true;
- }
- }
-
- return super.onKeyDown(keyCode, event);
- }
-
- public void onItemClick(AdapterView parent, View v, int position, long id) {
- ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
- if (entry != null) {
- Intent intent = entry.intent;
- if (intent != null) {
- try {
- startActivity(intent);
- } catch (ActivityNotFoundException e) {
- Log.e(TAG, "No activity found for intent: " + intent);
- signalError();
- }
- } else {
- signalError();
- }
- } else {
- signalError();
- }
- }
-
- /**
- * Signal an error to the user via a beep, or some other method.
- */
- private void signalError() {
- //TODO: implement this when we have the sonification APIs
- }
-
- /**
- * Build up the entries to display on the screen.
- *
- * @param personCursor the URI for the contact being displayed
- */
- 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;
-
- mWritableRawContactIds.clear();
-
- final Context context = this;
- final Sources sources = Sources.getInstance(context);
-
- // Build up method entries
- if (mLookupUri != null) {
- for (Entity entity: mEntities) {
- 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, this,
- ContactsSource.LEVEL_MIMETYPES);
- if (kind == null) continue;
-
- final ViewEntry entry = ViewEntry.fromValues(context, 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 = mStatuses.get(entry.id);
- if (status != null) {
- final String imMime = Im.CONTENT_ITEM_TYPE;
- final DataKind imKind = sources.getKindOrFallback(accountType,
- imMime, this, ContactsSource.LEVEL_MIMETYPES);
- final ViewEntry imEntry = ViewEntry.fromValues(context,
- 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 = getString(R.string.chat).toLowerCase();
- }
-
- // Apply presence and status details when available
- final DataStatus status = mStatuses.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 = (mNameRawContactId == rawContactId);
-
- final boolean duplicatesTitle =
- isNameRawContact
- && mDisplayNameSource == 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 = (mNameRawContactId == rawContactId);
-
- final boolean duplicatesTitle =
- isNameRawContact
- && mDisplayNameSource == 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 = mStatuses.get(entry.id);
- final boolean hasSocial = kind.actionBodySocial && status != null;
- if (hasSocial) {
- entry.applyStatus(status, true);
- }
-
- if (hasSocial || hasData) {
- mOtherEntries.add(entry);
- }
- }
- }
- }
- }
- }
-
- 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();
- }
-
- 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 */
- 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
- ViewEntry entry;
- }
-
- private final class ViewAdapter extends ContactEntryAdapter<ViewEntry>
- implements View.OnClickListener {
-
-
- ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
- super(context, sections, SHOW_SEPARATORS);
- }
-
- public void onClick(View v) {
- Intent intent = (Intent) v.getTag();
- startActivity(intent);
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- ViewEntry entry = getEntry(mSections, position, false);
- View v;
-
- ViewCache views;
-
- // Check to see if we can reuse convertView
- if (convertView != null) {
- v = convertView;
- views = (ViewCache) v.getTag();
- } else {
- // Create a new view if needed
- v = mInflater.inflate(R.layout.list_item_text_icons, parent, false);
-
- // Cache the children
- views = new ViewCache();
- views.label = (TextView) v.findViewById(android.R.id.text1);
- views.data = (TextView) v.findViewById(android.R.id.text2);
- views.footer = (TextView) v.findViewById(R.id.footer);
- views.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
- views.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon);
- views.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
- views.secondaryActionButton = (ImageView) v.findViewById(
- R.id.secondary_action_button);
- views.secondaryActionButton.setOnClickListener(this);
- views.secondaryActionDivider = v.findViewById(R.id.divider);
- v.setTag(views);
- }
-
- // Update the entry in the view cache
- views.entry = entry;
-
- // Bind the data to the view
- bindView(v, entry);
- return v;
- }
-
- @Override
- protected View newView(int position, ViewGroup parent) {
- // getView() handles this
- throw new UnsupportedOperationException();
- }
-
- @Override
- protected void bindView(View view, ViewEntry entry) {
- final Resources resources = mContext.getResources();
- ViewCache views = (ViewCache) view.getTag();
-
- // Set the label
- TextView label = views.label;
- setMaxLines(label, entry.maxLabelLines);
- label.setText(entry.label);
-
- // Set the data
- TextView data = views.data;
- if (data != null) {
- if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
- || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
- data.setText(PhoneNumberUtils.formatNumber(entry.data));
- } else {
- data.setText(entry.data);
- }
- setMaxLines(data, entry.maxLines);
- }
-
- // Set the footer
- if (!TextUtils.isEmpty(entry.footerLine)) {
- views.footer.setText(entry.footerLine);
- views.footer.setVisibility(View.VISIBLE);
- } else {
- views.footer.setVisibility(View.GONE);
- }
-
- // Set the primary icon
- views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
-
- // Set the action icon
- ImageView action = views.actionIcon;
- if (entry.actionIcon != -1) {
- Drawable actionIcon;
- if (entry.resPackageName != null) {
- // Load external resources through PackageManager
- actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName,
- entry.actionIcon, null);
- } else {
- actionIcon = resources.getDrawable(entry.actionIcon);
- }
- action.setImageDrawable(actionIcon);
- action.setVisibility(View.VISIBLE);
- } else {
- // Things should still line up as if there was an icon, so make it invisible
- action.setVisibility(View.INVISIBLE);
- }
-
- // Set the presence icon
- Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
- mContext, entry.presence);
- ImageView presenceIconView = views.presenceIcon;
- if (presenceIcon != null) {
- presenceIconView.setImageDrawable(presenceIcon);
- presenceIconView.setVisibility(View.VISIBLE);
- } else {
- presenceIconView.setVisibility(View.GONE);
- }
-
- // Set the secondary action button
- ImageView secondaryActionView = views.secondaryActionButton;
- Drawable secondaryActionIcon = null;
- if (entry.secondaryActionIcon != -1) {
- secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
- }
- if (entry.secondaryIntent != null && secondaryActionIcon != null) {
- secondaryActionView.setImageDrawable(secondaryActionIcon);
- secondaryActionView.setTag(entry.secondaryIntent);
- secondaryActionView.setVisibility(View.VISIBLE);
- views.secondaryActionDivider.setVisibility(View.VISIBLE);
- } else {
- secondaryActionView.setVisibility(View.GONE);
- views.secondaryActionDivider.setVisibility(View.GONE);
- }
- }
-
- private void setMaxLines(TextView textView, int maxLines) {
- if (maxLines == 1) {
- textView.setSingleLine(true);
- textView.setEllipsize(TextUtils.TruncateAt.END);
- } else {
- textView.setSingleLine(false);
- textView.setMaxLines(maxLines);
- textView.setEllipsize(null);
- }
- }
- }
-
- private 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;
- }
-
- @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);
- }
- }
-}
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
new file mode 100644
index 0000000..5894f77
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactDetailActivity.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.activities;
+
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+import com.android.contacts.views.detail.ContactDetailFragment;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+
+public class ContactDetailActivity extends Activity {
+ private static final String TAG = "ContactDetailActivity";
+
+ private ContactDetailFragment mFragment;
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+
+ setContentView(R.layout.contact_detail_activity);
+
+ mFragment = (ContactDetailFragment) findFragmentById(R.id.contact_detail_fragment);
+ mFragment.setListener(mFragmentListener);
+ mFragment.loadUri(getIntent().getData());
+
+ Log.i(TAG, getIntent().getData().toString());
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id, Bundle args) {
+ // ask the Fragment whether it knows about the dialog
+ final Dialog fragmentResult = mFragment.onCreateDialog(id, args);
+ if (fragmentResult != null) return fragmentResult;
+
+ // Nobody knows about the Dialog
+ Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args);
+ return null;
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ // TODO: This is too hardwired.
+ if (mFragment.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 (mFragment.onKeyDown(keyCode, event)) return true;
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private final ContactDetailFragment.Listener mFragmentListener =
+ new ContactDetailFragment.Listener() {
+ public void onContactNotFound() {
+ finish();
+ }
+
+ public void onEditRequested(Uri rawContactUri) {
+ startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
+ }
+
+ public void onItemClicked(Intent intent) {
+ startActivity(intent);
+ }
+
+ public void onDialogRequested(int id, Bundle bundle) {
+ showDialog(id, bundle);
+ }
+ };
+}
diff --git a/src/com/android/contacts/activities/ContactEditorActivity.java b/src/com/android/contacts/activities/ContactEditorActivity.java
new file mode 100644
index 0000000..1007410
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactEditorActivity.java
@@ -0,0 +1,98 @@
+/*
+ * 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.views.editor.ContactEditorFragment;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class ContactEditorActivity extends Activity {
+ private static final String TAG = "ContactEditorActivity";
+
+ private ContactEditorFragment mFragment;
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+
+ setContentView(R.layout.contact_editor_activity);
+
+ mFragment = (ContactEditorFragment) findFragmentById(R.id.contact_editor_fragment);
+ mFragment.setListener(mFragmentListener);
+ mFragment.loadUri(getIntent().getData());
+
+ Log.i(TAG, getIntent().getData().toString());
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id, Bundle args) {
+ // ask the Fragment whether it knows about the dialog
+ final Dialog fragmentResult = mFragment.onCreateDialog(id, args);
+ if (fragmentResult != null) return fragmentResult;
+
+ // Nobody knows about the Dialog
+ Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args);
+ return null;
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ // TODO: This is too hardwired.
+ if (mFragment.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);
+ }
+ }
+
+ private final ContactEditorFragment.Listener mFragmentListener =
+ new ContactEditorFragment.Listener() {
+ public void onContactNotFound() {
+ // TODO: Show error
+ finish();
+ }
+
+ public void onError() {
+ // TODO: Show error message
+ finish();
+ }
+
+ public void onEditorRequested(Intent intent) {
+ startActivity(intent);
+ }
+
+ public void onDialogRequested(int id, Bundle bundle) {
+ showDialog(id, bundle);
+ }
+ };
+}
diff --git a/src/com/android/contacts/activities/TwoPaneActivity.java b/src/com/android/contacts/activities/TwoPaneActivity.java
new file mode 100644
index 0000000..3f8a139
--- /dev/null
+++ b/src/com/android/contacts/activities/TwoPaneActivity.java
@@ -0,0 +1,201 @@
+/*
+ * 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.R;
+import com.android.contacts.list.DefaultContactBrowseListFragment;
+import com.android.contacts.list.OnContactBrowserActionListener;
+import com.android.contacts.views.detail.ContactDetailFragment;
+import com.android.contacts.views.editor.ContactEditorFragment;
+import com.android.contacts.widget.SearchEditText;
+import com.android.contacts.widget.SearchEditText.OnFilterTextListener;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+public class TwoPaneActivity extends Activity {
+ private final static String TAG = "TwoPaneActivity";
+
+ private DefaultContactBrowseListFragment mListFragment;
+ private ListFragmentListener mListFragmentListener = new ListFragmentListener();
+
+ private ContactDetailFragment mDetailFragment;
+ private DetailFragmentListener mDetailFragmentListener = new DetailFragmentListener();
+
+ private ContactEditorFragment mEditorFragment;
+ private EditorFragmentListener mEditorFragmentListener = new EditorFragmentListener();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.two_pane_activity);
+
+ mListFragment = (DefaultContactBrowseListFragment) findFragmentById(R.id.two_pane_list);
+ mListFragment.setOnContactListActionListener(mListFragmentListener);
+
+ setupContactDetailFragment();
+
+ setupSearchUI();
+ }
+
+ private void setupContactDetailFragment() {
+ // No editor here
+ if (mEditorFragment != null) {
+ mEditorFragment.setListener(null);
+ mEditorFragment = null;
+ }
+
+ // Already showing? Nothing to do
+ if (mDetailFragment != null) return;
+
+ mDetailFragment = new ContactDetailFragment();
+ mDetailFragment.setListener(mDetailFragmentListener);
+
+ // Nothing showing yet? Create (this happens during Activity-Startup)
+ openFragmentTransaction()
+ .replace(R.id.two_pane_right_view, mDetailFragment)
+ .commit();
+
+ }
+
+ private void setupContactEditorFragment() {
+ // No detail view here
+ if (mDetailFragment != null) {
+ mDetailFragment.setListener(null);
+ mDetailFragment = null;
+ }
+
+ // Already showing? Nothing to do
+ if (mEditorFragment != null) return;
+
+ mEditorFragment = new ContactEditorFragment();
+ mEditorFragment.setListener(mEditorFragmentListener);
+
+ // Nothing showing yet? Create (this happens during Activity-Startup)
+ openFragmentTransaction()
+ .replace(R.id.two_pane_right_view, mEditorFragment)
+ .commit();
+
+ }
+
+ private void setupSearchUI() {
+ SearchEditText searchEditText = (SearchEditText)findViewById(R.id.search_src_text);
+ searchEditText.setOnFilterTextListener(new OnFilterTextListener() {
+ public void onFilterChange(String queryString) {
+ mListFragment.setSearchMode(!TextUtils.isEmpty(queryString));
+ mListFragment.setQueryString(queryString);
+ }
+
+ public void onCancelSearch() {
+ }
+ });
+ }
+
+ private class ListFragmentListener implements OnContactBrowserActionListener {
+ public void onAddToFavoritesAction(Uri contactUri) {
+ Toast.makeText(TwoPaneActivity.this, "onAddToFavoritesAction",
+ Toast.LENGTH_LONG).show();
+ }
+
+ public void onCallContactAction(Uri contactUri) {
+ Toast.makeText(TwoPaneActivity.this, "onCallContactAction",
+ Toast.LENGTH_LONG).show();
+ }
+
+ public void onCreateNewContactAction() {
+ Toast.makeText(TwoPaneActivity.this, "onCreateNewContactAction",
+ Toast.LENGTH_LONG).show();
+ }
+
+ public void onDeleteContactAction(Uri contactUri) {
+ Toast.makeText(TwoPaneActivity.this, "onDeleteContactAction",
+ Toast.LENGTH_LONG).show();
+ }
+
+ public void onEditContactAction(Uri contactLookupUri) {
+ Toast.makeText(TwoPaneActivity.this, "onEditContactAction",
+ Toast.LENGTH_LONG).show();
+ }
+
+ public void onFinishAction() {
+ Toast.makeText(TwoPaneActivity.this, "onFinishAction",
+ Toast.LENGTH_LONG).show();
+ }
+
+ public void onRemoveFromFavoritesAction(Uri contactUri) {
+ Toast.makeText(TwoPaneActivity.this, "onRemoveFromFavoritesAction",
+ Toast.LENGTH_LONG).show();
+ }
+
+ public void onSearchAllContactsAction(String string) {
+ Toast.makeText(TwoPaneActivity.this, "onSearchAllContactsAction",
+ Toast.LENGTH_LONG).show();
+ }
+
+ public void onSmsContactAction(Uri contactUri) {
+ Toast.makeText(TwoPaneActivity.this, "onSmsContactAction",
+ Toast.LENGTH_LONG).show();
+ }
+
+ public void onViewContactAction(Uri contactLookupUri) {
+ setupContactDetailFragment();
+ mDetailFragment.loadUri(contactLookupUri);
+ }
+ }
+
+ private class DetailFragmentListener implements ContactDetailFragment.Listener {
+ public void onContactNotFound() {
+ Toast.makeText(TwoPaneActivity.this, "onContactNotFound", Toast.LENGTH_LONG).show();
+ }
+
+ public void onEditRequested(Uri contactLookupUri) {
+ setupContactEditorFragment();
+ mEditorFragment.loadUri(contactLookupUri);
+ }
+
+ public void onItemClicked(Intent intent) {
+ startActivity(intent);
+ }
+
+ public void onDialogRequested(int id, Bundle bundle) {
+ showDialog(id, bundle);
+ }
+ }
+
+ private class EditorFragmentListener implements ContactEditorFragment.Listener {
+ public void onContactNotFound() {
+ Toast.makeText(TwoPaneActivity.this, "onContactNotFound", Toast.LENGTH_LONG).show();
+ }
+
+ public void onDialogRequested(int id, Bundle bundle) {
+ Toast.makeText(TwoPaneActivity.this, "onDialogRequested", Toast.LENGTH_LONG).show();
+ }
+
+ public void onEditorRequested(Intent intent) {
+ Toast.makeText(TwoPaneActivity.this, "onEditorRequested", Toast.LENGTH_LONG).show();
+ }
+
+ public void onError() {
+ Toast.makeText(TwoPaneActivity.this, "onError", Toast.LENGTH_LONG).show();
+ }
+ }
+}
diff --git a/src/com/android/contacts/list/CallOrSmsInitiator.java b/src/com/android/contacts/list/CallOrSmsInitiator.java
new file mode 100644
index 0000000..b5aabee
--- /dev/null
+++ b/src/com/android/contacts/list/CallOrSmsInitiator.java
@@ -0,0 +1,139 @@
+/*
+ * 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.list;
+
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.PhoneDisambigDialog;
+import com.android.internal.widget.RotarySelector.OnDialTriggerListener;
+
+import android.content.AsyncQueryHandler;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts.Data;
+
+/**
+ * Initiates phone calls or SMS messages.
+ */
+public class CallOrSmsInitiator {
+
+ private final Context mContext;
+ private AsyncQueryHandler mQueryHandler;
+ private int mCurrentToken;
+ private boolean mSendSms;
+
+ private static final String[] PHONE_NUMBER_PROJECTION = new String[] {
+ Phone._ID,
+ Phone.NUMBER,
+ Phone.IS_SUPER_PRIMARY,
+ RawContacts.ACCOUNT_TYPE,
+ Phone.TYPE,
+ Phone.LABEL
+ };
+
+ private static final String PHONE_NUMBER_SELECTION = Data.MIMETYPE + "='"
+ + Phone.CONTENT_ITEM_TYPE + "' AND " + Phone.NUMBER + " NOT NULL";
+ private OnDismissListener mDismissListener;
+
+ public CallOrSmsInitiator(Context context) {
+ this.mContext = context;
+ mQueryHandler = new AsyncQueryHandler(context.getContentResolver()) {
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ onPhoneNumberQueryComplete(token, cookie, cursor);
+ }
+ };
+ }
+
+ public void setOnDismissListener(DialogInterface.OnDismissListener dismissListener) {
+ this.mDismissListener = dismissListener;
+ }
+
+ protected void onPhoneNumberQueryComplete(int token, Object cookie, Cursor cursor) {
+ if (cursor == null || cursor.getCount() == 0) {
+ cursor.close();
+ return;
+ }
+
+ if (token != mCurrentToken) { // Stale query, ignore
+ cursor.close();
+ return;
+ }
+
+ String phone = null;
+ if (cursor.getCount() == 1) {
+ // only one number, call it.
+ cursor.moveToFirst();
+ phone = cursor.getString(cursor.getColumnIndex(Phone.NUMBER));
+ } else {
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ if (cursor.getInt(cursor.getColumnIndex(Phone.IS_SUPER_PRIMARY)) != 0) {
+ // Found super primary, call it.
+ phone = cursor.getString(cursor.getColumnIndex(Phone.NUMBER));
+ break;
+ }
+ }
+ }
+
+ if (phone == null) {
+ // Display dialog to choose a number to call.
+ PhoneDisambigDialog phoneDialog = new PhoneDisambigDialog(mContext, cursor, mSendSms);
+ if (mDismissListener != null) {
+ phoneDialog.setOnDismissListener(mDismissListener);
+ }
+ phoneDialog.show();
+ } else {
+ if (mSendSms) {
+ ContactsUtils.initiateSms(mContext, phone);
+ } else {
+ ContactsUtils.initiateCall(mContext, phone);
+ }
+ if (mDismissListener != null) {
+ mDismissListener.onDismiss(null);
+ }
+ }
+ }
+
+ /**
+ * Initiates a phone call with the specified contact. If necessary, displays
+ * a disambiguation dialog to see which number to call.
+ */
+ public void initiateCall(Uri contactUri) {
+ callOrSendSms(contactUri, false);
+ }
+
+ /**
+ * Initiates a text message to the specified contact. If necessary, displays
+ * a disambiguation dialog to see which number to call.
+ */
+ public void initiateSms(Uri contactUri) {
+ callOrSendSms(contactUri, true);
+ }
+
+ private void callOrSendSms(Uri contactUri, boolean sendSms) {
+ mCurrentToken++;
+ mSendSms = sendSms;
+ Uri dataUri = Uri.withAppendedPath(contactUri, Contacts.Data.CONTENT_DIRECTORY);
+ mQueryHandler.startQuery(mCurrentToken, dataUri, dataUri, PHONE_NUMBER_PROJECTION,
+ PHONE_NUMBER_SELECTION, null, null);
+ }
+}
diff --git a/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java b/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
new file mode 100644
index 0000000..1529c1d
--- /dev/null
+++ b/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
@@ -0,0 +1,139 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+import com.android.contacts.widget.ContextMenuAdapter;
+
+import android.net.Uri;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView;
+
+/**
+ * A contextual menu adapter for the basic contact list.
+ */
+public class ContactBrowseListContextMenuAdapter implements ContextMenuAdapter {
+
+ private static final int MENU_ITEM_VIEW_CONTACT = 1;
+ private static final int MENU_ITEM_CALL = 2;
+ private static final int MENU_ITEM_SEND_SMS = 3;
+ private static final int MENU_ITEM_EDIT = 4;
+ private static final int MENU_ITEM_DELETE = 5;
+ private static final int MENU_ITEM_TOGGLE_STAR = 6;
+
+ private static final String TAG = "LightContactBrowserContextMenuAdapter";
+
+ private final ContactBrowseListFragment mContactListFragment;
+
+ public ContactBrowseListContextMenuAdapter(ContactBrowseListFragment fragment) {
+ this.mContactListFragment = fragment;
+ }
+
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
+ AdapterView.AdapterContextMenuInfo info;
+ try {
+ info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ } catch (ClassCastException e) {
+ Log.wtf(TAG, "Bad menuInfo", e);
+ return;
+ }
+
+ ContactListAdapter adapter = mContactListFragment.getAdapter();
+ int headerViewsCount = mContactListFragment.getListView().getHeaderViewsCount();
+ int position = info.position - headerViewsCount;
+
+ // Setup the menu header
+ menu.setHeaderTitle(adapter.getContactDisplayName(position));
+
+ // View contact details
+ menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact);
+
+ if (adapter.getHasPhoneNumber(position)) {
+ // Calling contact
+ menu.add(0, MENU_ITEM_CALL, 0, R.string.menu_call);
+ // Send SMS item
+ menu.add(0, MENU_ITEM_SEND_SMS, 0, R.string.menu_sendSMS);
+ }
+
+ // Star toggling
+ if (!adapter.isContactStarred(position)) {
+ menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar);
+ } else {
+ menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar);
+ }
+
+ // Contact editing
+ menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact);
+ menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact);
+ }
+
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info;
+ try {
+ info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ } catch (ClassCastException e) {
+ Log.wtf(TAG, "Bad menuInfo", e);
+ return false;
+ }
+
+ ContactListAdapter adapter = mContactListFragment.getAdapter();
+ int headerViewsCount = mContactListFragment.getListView().getHeaderViewsCount();
+ int position = info.position - headerViewsCount;
+
+ final Uri contactUri = adapter.getContactUri(position);
+ switch (item.getItemId()) {
+ case MENU_ITEM_VIEW_CONTACT: {
+ mContactListFragment.viewContact(contactUri);
+ return true;
+ }
+
+ case MENU_ITEM_TOGGLE_STAR: {
+ if (adapter.isContactStarred(position)) {
+ mContactListFragment.removeFromFavorites(contactUri);
+ } else {
+ mContactListFragment.addToFavorites(contactUri);
+ }
+ return true;
+ }
+
+ case MENU_ITEM_CALL: {
+ mContactListFragment.callContact(contactUri);
+ return true;
+ }
+
+ case MENU_ITEM_SEND_SMS: {
+ mContactListFragment.smsContact(contactUri);
+ return true;
+ }
+
+ case MENU_ITEM_EDIT: {
+ mContactListFragment.editContact(contactUri);
+ return true;
+ }
+
+ case MENU_ITEM_DELETE: {
+ mContactListFragment.deleteContact(contactUri);
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/com/android/contacts/list/ContactBrowseListFragment.java b/src/com/android/contacts/list/ContactBrowseListFragment.java
new file mode 100644
index 0000000..05f2364
--- /dev/null
+++ b/src/com/android/contacts/list/ContactBrowseListFragment.java
@@ -0,0 +1,97 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.net.Uri;
+
+/**
+ * Fragment containing a contact list used for browsing (as compared to
+ * picking a contact with one of the PICK intents).
+ */
+public abstract class ContactBrowseListFragment extends
+ ContactEntryListFragment<ContactListAdapter> {
+
+ private OnContactBrowserActionListener mListener;
+
+ @Override
+ protected void prepareEmptyView() {
+ if (isSearchMode()) {
+ return;
+ } else if (isSearchResultsMode()) {
+ setEmptyText(R.string.noMatchingContacts);
+ } else if (isSyncActive()) {
+ if (hasIccCard()) {
+ setEmptyText(R.string.noContactsHelpTextWithSync);
+ } else {
+ setEmptyText(R.string.noContactsNoSimHelpTextWithSync);
+ }
+ } else {
+ if (hasIccCard()) {
+ setEmptyText(R.string.noContactsHelpText);
+ } else {
+ setEmptyText(R.string.noContactsNoSimHelpText);
+ }
+ }
+ }
+
+ public void setOnContactListActionListener(OnContactBrowserActionListener listener) {
+ mListener = listener;
+ }
+
+ public void createNewContact() {
+ mListener.onCreateNewContactAction();
+ }
+
+ public void searchAllContacts() {
+ mListener.onSearchAllContactsAction((String)null);
+ }
+
+ public void viewContact(Uri contactUri) {
+ mListener.onViewContactAction(contactUri);
+ }
+
+ public void editContact(Uri contactUri) {
+ mListener.onEditContactAction(contactUri);
+ }
+
+ public void deleteContact(Uri contactUri) {
+ mListener.onDeleteContactAction(contactUri);
+ }
+
+ public void addToFavorites(Uri contactUri) {
+ mListener.onAddToFavoritesAction(contactUri);
+ }
+
+ public void removeFromFavorites(Uri contactUri) {
+ mListener.onRemoveFromFavoritesAction(contactUri);
+ }
+
+ public void callContact(Uri contactUri) {
+ mListener.onCallContactAction(contactUri);
+ }
+
+ public void smsContact(Uri contactUri) {
+ mListener.onSmsContactAction(contactUri);
+ }
+
+ @Override
+ protected void finish() {
+ super.finish();
+ mListener.onFinishAction();
+ }
+}
diff --git a/src/com/android/contacts/list/ContactEntryListAdapter.java b/src/com/android/contacts/list/ContactEntryListAdapter.java
new file mode 100644
index 0000000..ff402a2
--- /dev/null
+++ b/src/com/android/contacts/list/ContactEntryListAdapter.java
@@ -0,0 +1,398 @@
+/*
+ * 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.list;
+
+import com.android.contacts.ContactPhotoLoader;
+import com.android.contacts.ContactsSectionIndexer;
+import com.android.contacts.R;
+import com.android.contacts.widget.IndexerListAdapter;
+import com.android.contacts.widget.TextWithHighlightingFactory;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import java.util.HashSet;
+
+/**
+ * Common base class for various contact-related lists, e.g. contact list, phone number list
+ * etc.
+ */
+public abstract class ContactEntryListAdapter extends IndexerListAdapter {
+
+ private static final String TAG = "ContactEntryListAdapter";
+
+ private static final class DirectoryQuery {
+ public static final Uri URI = Directory.CONTENT_URI;
+ public static final String ORDER_BY = Directory._ID;
+
+ public static final String[] PROJECTION = {
+ Directory._ID,
+ Directory.PACKAGE_NAME,
+ Directory.TYPE_RESOURCE_ID,
+ Directory.DISPLAY_NAME,
+ };
+
+ public static final int ID = 0;
+ public static final int PACKAGE_NAME = 1;
+ public static final int TYPE_RESOURCE_ID = 2;
+ public static final int DISPLAY_NAME = 3;
+ }
+
+ /**
+ * The animation is used here to allocate animated name text views.
+ */
+ private TextWithHighlightingFactory mTextWithHighlightingFactory;
+ private int mDisplayOrder;
+ private int mSortOrder;
+ private boolean mNameHighlightingEnabled;
+
+ private boolean mDisplayPhotos;
+ private ContactPhotoLoader mPhotoLoader;
+
+ private String mQueryString;
+ private boolean mSearchMode;
+ private boolean mSearchResultsMode;
+
+ private boolean mLoading = true;
+ private boolean mEmptyListEnabled = true;
+
+ public ContactEntryListAdapter(Context context) {
+ super(context, R.layout.list_section, R.id.header_text);
+ addPartitions();
+ }
+
+ protected void addPartitions() {
+ addPartition(createDefaultDirectoryPartition());
+ }
+
+ protected DirectoryPartition createDefaultDirectoryPartition() {
+ DirectoryPartition partition = new DirectoryPartition(true, true);
+ partition.setDirectoryId(Directory.DEFAULT);
+ partition.setDirectoryType(getContext().getString(R.string.contactsList));
+ partition.setPriorityDirectory(true);
+ return partition;
+ }
+
+ private int getPartitionByDirectoryId(long id) {
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ if (((DirectoryPartition)partition).getDirectoryId() == id) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ public abstract String getContactDisplayName(int position);
+ public abstract void configureLoader(CursorLoader loader, long directoryId);
+
+ /**
+ * Marks all partitions as "loading"
+ */
+ public void onDataReload() {
+ boolean notify = false;
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ DirectoryPartition directoryPartition = (DirectoryPartition)partition;
+ if (!directoryPartition.isLoading()) {
+ directoryPartition.setLoading(true);
+ notify = true;
+ }
+ }
+ }
+ if (notify) {
+ notifyDataSetChanged();
+ }
+ }
+
+ public boolean isSearchMode() {
+ return mSearchMode;
+ }
+
+ public void setSearchMode(boolean flag) {
+ mSearchMode = flag;
+
+ int defaultPartitionIndex = -1;
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition &&
+ ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) {
+ defaultPartitionIndex = i;
+ break;
+ }
+ }
+ if (defaultPartitionIndex != -1) {
+ setShowIfEmpty(defaultPartitionIndex, flag);
+ setHasHeader(defaultPartitionIndex, flag);
+ }
+ }
+
+ public boolean isSearchResultsMode() {
+ return mSearchResultsMode;
+ }
+
+ public void setSearchResultsMode(boolean searchResultsMode) {
+ mSearchResultsMode = searchResultsMode;
+ }
+
+ public String getQueryString() {
+ return mQueryString;
+ }
+
+ public void setQueryString(String queryString) {
+ mQueryString = queryString;
+ }
+
+ public int getContactNameDisplayOrder() {
+ return mDisplayOrder;
+ }
+
+ public void setContactNameDisplayOrder(int displayOrder) {
+ mDisplayOrder = displayOrder;
+ }
+
+ public int getSortOrder() {
+ return mSortOrder;
+ }
+
+ public void setSortOrder(int sortOrder) {
+ mSortOrder = sortOrder;
+ }
+
+ public void setNameHighlightingEnabled(boolean flag) {
+ mNameHighlightingEnabled = flag;
+ }
+
+ public boolean isNameHighlightingEnabled() {
+ return mNameHighlightingEnabled;
+ }
+
+ public void setTextWithHighlightingFactory(TextWithHighlightingFactory factory) {
+ mTextWithHighlightingFactory = factory;
+ }
+
+ protected TextWithHighlightingFactory getTextWithHighlightingFactory() {
+ return mTextWithHighlightingFactory;
+ }
+
+ public void setPhotoLoader(ContactPhotoLoader photoLoader) {
+ mPhotoLoader = photoLoader;
+ }
+
+ protected ContactPhotoLoader getPhotoLoader() {
+ return mPhotoLoader;
+ }
+
+ public boolean getDisplayPhotos() {
+ return mDisplayPhotos;
+ }
+
+ public void setDisplayPhotos(boolean displayPhotos) {
+ mDisplayPhotos = displayPhotos;
+ }
+
+ public boolean isEmptyListEnabled() {
+ return mEmptyListEnabled;
+ }
+
+ public void setEmptyListEnabled(boolean flag) {
+ mEmptyListEnabled = flag;
+ }
+
+ public void configureDirectoryLoader(CursorLoader loader) {
+ loader.setUri(DirectoryQuery.URI);
+ loader.setProjection(DirectoryQuery.PROJECTION);
+ loader.setSortOrder(DirectoryQuery.ORDER_BY);
+ }
+
+ /**
+ * Updates partitions according to the directory meta-data contained in the supplied
+ * cursor. Takes ownership of the cursor and will close it.
+ */
+ public void changeDirectories(Cursor cursor) {
+ HashSet<Long> directoryIds = new HashSet<Long>();
+
+ // TODO preserve the order of partition to match those of the cursor
+ // Phase I: add new directories
+ try {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(DirectoryQuery.ID);
+ directoryIds.add(id);
+ if (getPartitionByDirectoryId(id) == -1) {
+ DirectoryPartition partition = createDirectoryPartition(cursor);
+ addPartition(partition);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+
+ // Phase II: remove deleted directories
+ int count = getPartitionCount();
+ for (int i = count; --i >= 0; ) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ long id = ((DirectoryPartition)partition).getDirectoryId();
+ if (!directoryIds.contains(id)) {
+ removePartition(i);
+ }
+ }
+ }
+
+ invalidate();
+ notifyDataSetChanged();
+ }
+
+ private DirectoryPartition createDirectoryPartition(Cursor cursor) {
+ PackageManager pm = getContext().getPackageManager();
+ DirectoryPartition partition = new DirectoryPartition(false, true);
+ partition.setDirectoryId(cursor.getLong(DirectoryQuery.ID));
+ String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
+ int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+ if (!TextUtils.isEmpty(packageName) && typeResourceId != 0) {
+ // TODO: should this be done on a background thread?
+ try {
+ partition.setDirectoryType(pm.getResourcesForApplication(packageName)
+ .getString(typeResourceId));
+ } catch (Exception e) {
+ Log.e(TAG, "Cannot obtain directory type from package: " + packageName);
+ }
+ }
+ partition.setDisplayName(cursor.getString(DirectoryQuery.DISPLAY_NAME));
+ return partition;
+ }
+
+ @Override
+ public void changeCursor(int partitionIndex, Cursor cursor) {
+ Partition partition = getPartition(partitionIndex);
+ if (partition instanceof DirectoryPartition) {
+ ((DirectoryPartition)partition).setLoading(false);
+ }
+
+ super.changeCursor(partitionIndex, cursor);
+
+ if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
+ updateIndexer(cursor);
+ }
+ }
+
+ public void changeCursor(Cursor cursor) {
+ changeCursor(0, cursor);
+ }
+
+ /**
+ * Updates the indexer, which is used to produce section headers.
+ */
+ private void updateIndexer(Cursor cursor) {
+ if (cursor == null) {
+ setIndexer(null);
+ return;
+ }
+
+ Bundle bundle = cursor.getExtras();
+ if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) {
+ String sections[] =
+ bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
+ int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
+ setIndexer(new ContactsSectionIndexer(sections, counts));
+ } else {
+ setIndexer(null);
+ }
+ }
+
+ @Override
+ public boolean isEmpty() {
+ // TODO
+// if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) {
+// return true;
+// }
+
+ if (!mEmptyListEnabled) {
+ return false;
+ } else if (isSearchMode()) {
+ return TextUtils.isEmpty(getQueryString());
+ } else if (mLoading) {
+ // We don't want the empty state to show when loading.
+ return false;
+ } else {
+ return super.isEmpty();
+ }
+ }
+
+ @Override
+ protected View newHeaderView(Context context, int partition, Cursor cursor,
+ ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ return inflater.inflate(R.layout.directory_header, parent, false);
+ }
+
+ @Override
+ protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) {
+ Partition partition = getPartition(partitionIndex);
+ if (!(partition instanceof DirectoryPartition)) {
+ return;
+ }
+
+ DirectoryPartition directoryPartition = (DirectoryPartition)partition;
+ TextView directoryTypeTextView = (TextView)view.findViewById(R.id.directory_type);
+ directoryTypeTextView.setText(directoryPartition.getDirectoryType());
+ TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name);
+ if (!TextUtils.isEmpty(directoryPartition.getDisplayName())) {
+ displayNameTextView.setText(directoryPartition.getDisplayName());
+ displayNameTextView.setVisibility(View.VISIBLE);
+ } else {
+ displayNameTextView.setVisibility(View.GONE);
+ }
+
+ TextView countText = (TextView)view.findViewById(R.id.count);
+ if (directoryPartition.isLoading()) {
+ countText.setText(R.string.search_results_searching);
+ } else {
+ int count = cursor == null ? 0 : cursor.getCount();
+ countText.setText(getQuantityText(count, R.string.listFoundAllContactsZero,
+ R.plurals.searchFoundContacts));
+ }
+ }
+
+ // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
+ public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
+ if (count == 0) {
+ return getContext().getString(zeroResourceId);
+ } else {
+ String format = getContext().getResources()
+ .getQuantityText(pluralResourceId, count).toString();
+ return String.format(format, count);
+ }
+ }
+}
diff --git a/src/com/android/contacts/list/ContactEntryListFragment.java b/src/com/android/contacts/list/ContactEntryListFragment.java
new file mode 100644
index 0000000..9bfdbb3
--- /dev/null
+++ b/src/com/android/contacts/list/ContactEntryListFragment.java
@@ -0,0 +1,865 @@
+/*
+ * 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.list;
+
+import com.android.contacts.ContactEntryListView;
+import com.android.contacts.ContactListEmptyView;
+import com.android.contacts.ContactPhotoLoader;
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+import com.android.contacts.ui.ContactsPreferences;
+import com.android.contacts.widget.ContextMenuAdapter;
+import com.android.contacts.widget.CompositeCursorAdapter.Partition;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.Activity;
+import android.app.LoaderManagingFragment;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.IContentService;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.ProviderStatus;
+import android.telephony.TelephonyManager;
+import android.text.Html;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.view.View.OnFocusChangeListener;
+import android.view.View.OnTouchListener;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView.OnItemClickListener;
+
+/**
+ * Common base class for various contact-related list fragments.
+ */
+public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter>
+ extends LoaderManagingFragment<Cursor>
+ implements OnItemClickListener, OnScrollListener, OnFocusChangeListener, OnTouchListener {
+
+ public static final int ACTIVITY_REQUEST_CODE_FILTER = 1;
+
+ private static final String TAG = "ContactEntryListFragment";
+
+ private static final String LIST_STATE_KEY = "liststate";
+
+ private static final String DIRECTORY_ID_ARG_KEY = "directoryId";
+
+ private static final int DIRECTORY_LOADER_ID = -1;
+
+ private boolean mSectionHeaderDisplayEnabled;
+ private boolean mPhotoLoaderEnabled;
+ private boolean mSearchMode;
+ private boolean mSearchResultsMode;
+ private boolean mAizyEnabled;
+ private String mQueryString;
+
+ private T mAdapter;
+ private View mView;
+ private ListView mListView;
+ private ContactListAizyView mAizy;
+
+ /**
+ * Used for keeping track of the scroll state of the list.
+ */
+ private Parcelable mListState;
+
+ private boolean mLegacyCompatibility;
+ private int mDisplayOrder;
+ private int mSortOrder;
+
+ private ContextMenuAdapter mContextMenuAdapter;
+ private ContactPhotoLoader mPhotoLoader;
+ private ContactListEmptyView mEmptyView;
+ private ProviderStatusLoader mProviderStatusLoader;
+ private ContactsPreferences mContactsPrefs;
+
+ private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
+
+ private boolean mForceLoad;
+ private boolean mLoadDirectoryList;
+
+ /**
+ * Indicates whether we are doing the initial complete load of data or
+ * a refresh caused by a change notification.
+ */
+ private boolean mLoadPriorityDirectoriesOnly;
+
+ private ContactsRequest mRequest;
+
+ protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
+ protected abstract T createListAdapter();
+
+ /**
+ * @param position Please note that the position is already adjusted for
+ * header views, so "0" means the first list item below header
+ * views.
+ */
+ protected abstract void onItemClick(int position, long id);
+
+ public T getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public View getView() {
+ return mView;
+ }
+
+ public ListView getListView() {
+ return mListView;
+ }
+
+ public ContactListEmptyView getEmptyView() {
+ return mEmptyView;
+ }
+
+ @Override
+ protected void onInitializeLoaders() {
+ }
+
+ /**
+ * Returns the parsed intent that started the activity hosting this fragment.
+ */
+ public ContactsRequest getContactsRequest() {
+ return mRequest;
+ }
+
+ /**
+ * Sets a parsed intent that started the activity hosting this fragment.
+ */
+ public void setContactsRequest(ContactsRequest request) {
+ mRequest = request;
+ }
+
+ @Override
+ public void onStart() {
+ if (mContactsPrefs == null) {
+ mContactsPrefs = new ContactsPreferences(getActivity());
+ }
+
+ if (mProviderStatusLoader == null) {
+ mProviderStatusLoader = new ProviderStatusLoader(getActivity());
+ }
+
+ loadPreferences(mContactsPrefs);
+
+ if (mListView instanceof ContactEntryListView) {
+ ContactEntryListView listView = (ContactEntryListView)mListView;
+ listView.setHighlightNamesWhenScrolling(isNameHighlighingEnabled());
+ }
+
+ mForceLoad = false;
+ mLoadDirectoryList = true;
+ mLoadPriorityDirectoriesOnly = true;
+ startLoading();
+ super.onStart();
+ }
+
+ private void startLoading() {
+ configureAdapter();
+ int partitionCount = mAdapter.getPartitionCount();
+ for (int i = 0; i < partitionCount; i++) {
+ Partition partition = mAdapter.getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ DirectoryPartition directoryPartition = (DirectoryPartition)partition;
+ if (mLoadPriorityDirectoriesOnly == directoryPartition.isPriorityDirectory()) {
+ startLoadingDirectoryPartition(i);
+ }
+ } else {
+ startLoading(i, null);
+ }
+ }
+ }
+
+ @Override
+ protected Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ CursorLoader loader = new CursorLoader(getActivity(), null, null, null, null, null);
+ if (id == DIRECTORY_LOADER_ID) {
+ mAdapter.configureDirectoryLoader(loader);
+ } else {
+ long directoryId = args != null && args.containsKey(DIRECTORY_ID_ARG_KEY)
+ ? args.getLong(DIRECTORY_ID_ARG_KEY)
+ : Directory.DEFAULT;
+ mAdapter.configureLoader(loader, directoryId);
+ }
+ return loader;
+ }
+
+ private void startLoadingDirectoryPartition(int partitionIndex) {
+ DirectoryPartition partition = (DirectoryPartition)mAdapter.getPartition(partitionIndex);
+ CursorLoader loader = (CursorLoader)getLoader(partitionIndex);
+ if (loader == null) {
+ Bundle args = new Bundle();
+ args.putLong(DIRECTORY_ID_ARG_KEY, partition.getDirectoryId());
+ startLoading(partitionIndex, args);
+ } else if (mForceLoad) {
+ mAdapter.configureLoader(loader, partition.getDirectoryId());
+ loader.forceLoad();
+ }
+ }
+
+ @Override
+ protected void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (!checkProviderStatus(false)) {
+ if (data != null) {
+ data.close();
+ }
+ return;
+ }
+
+ int loaderId = loader.getId();
+ if (loaderId == DIRECTORY_LOADER_ID) {
+ mAdapter.changeDirectories(data);
+ } else {
+ final int partitionIndex = loaderId; // by convention
+ mAdapter.changeCursor(partitionIndex, data);
+ showCount(partitionIndex, data);
+ if (partitionIndex == mAdapter.getIndexedPartition()) {
+ mAizy.setIndexer(mAdapter.getIndexer());
+ }
+ completeRestoreInstanceState();
+ }
+
+ if (isSearchMode()) {
+ if (mLoadDirectoryList) {
+ mLoadDirectoryList = false;
+ startLoading(DIRECTORY_LOADER_ID, null);
+ } else if (mLoadPriorityDirectoriesOnly) {
+ mLoadPriorityDirectoriesOnly = false;
+ startLoading();
+ }
+ }
+
+// TODO fix the empty view
+// if (mEmptyView != null && (data == null || data.getCount() == 0)) {
+// prepareEmptyView();
+// }
+ }
+
+ protected void reloadData() {
+ cancelLoading();
+ mAdapter.onDataReload();
+ mLoadPriorityDirectoriesOnly = true;
+ mForceLoad = true;
+ startLoading();
+ }
+
+ private void cancelLoading() {
+ int size = mAdapter.getPartitionCount();
+ for (int i = 0; i < size; i++) {
+ CursorLoader loader = (CursorLoader)getLoader(i);
+ if (loader != null) {
+ loader.cancelLoad();
+ }
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mAdapter.clearPartitions();
+ }
+
+ /**
+ * Configures the empty view. It is called when we are about to populate
+ * the list with an empty cursor.
+ */
+ protected void prepareEmptyView() {
+ }
+
+ /**
+ * Shows the count of entries included in the list. The default
+ * implementation does nothing.
+ */
+ protected void showCount(int partitionIndex, Cursor data) {
+ }
+
+ /**
+ * Provides logic that dismisses this fragment. The default implementation
+ * does nothing.
+ */
+ protected void finish() {
+ }
+
+ public void setSectionHeaderDisplayEnabled(boolean flag) {
+ mSectionHeaderDisplayEnabled = flag;
+ if (mAdapter != null) {
+ mAdapter.setSectionHeaderDisplayEnabled(flag);
+ }
+ configureAizy();
+ }
+
+ public boolean isSectionHeaderDisplayEnabled() {
+ return mSectionHeaderDisplayEnabled;
+ }
+
+ public void setAizyEnabled(boolean flag) {
+ mAizyEnabled = flag;
+ configureAizy();
+ }
+
+ public boolean isAizyEnabled() {
+ return mAizyEnabled;
+ }
+
+ private void configureAizy() {
+ boolean hasAisy = isAizyEnabled() && isSectionHeaderDisplayEnabled();
+
+ if (mListView != null) {
+ mListView.setFastScrollEnabled(!hasAisy);
+ mListView.setVerticalScrollBarEnabled(!hasAisy);
+ }
+ if (mAizy != null) {
+ mAizy.setVisibility(hasAisy ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ public void setPhotoLoaderEnabled(boolean flag) {
+ mPhotoLoaderEnabled = flag;
+ configurePhotoLoader();
+ }
+
+ public boolean isPhotoLoaderEnabled() {
+ return mPhotoLoaderEnabled;
+ }
+
+ public void setSearchMode(boolean flag) {
+ if (mSearchMode != flag) {
+ mSearchMode = flag;
+ setSectionHeaderDisplayEnabled(!mSearchMode);
+
+ if (mAdapter != null) {
+ mAdapter.clearPartitions();
+ mAdapter.setSearchMode(flag);
+ mAdapter.setPinnedPartitionHeadersEnabled(flag);
+ reloadData();
+ }
+
+ if (mListView != null) {
+ mListView.setFastScrollEnabled(!flag);
+ }
+ }
+ }
+
+ public boolean isSearchMode() {
+ return mSearchMode;
+ }
+
+ public void setSearchResultsMode(boolean flag) {
+ mSearchResultsMode = flag;
+ if (mAdapter != null) {
+ mAdapter.setSearchResultsMode(flag);
+ }
+ }
+
+ public boolean isSearchResultsMode() {
+ return mSearchResultsMode;
+ }
+
+ public String getQueryString() {
+ return mQueryString;
+ }
+
+ public void setQueryString(String queryString) {
+ if (!TextUtils.equals(mQueryString, queryString)) {
+ mQueryString = queryString;
+ if (mAdapter != null) {
+ mAdapter.setQueryString(queryString);
+ reloadData();
+ }
+ }
+ }
+
+ public boolean isLegacyCompatibilityMode() {
+ return mLegacyCompatibility;
+ }
+
+ public void setLegacyCompatibilityMode(boolean flag) {
+ mLegacyCompatibility = flag;
+ }
+
+ public int getContactNameDisplayOrder() {
+ return mDisplayOrder;
+ }
+
+ public void setContactNameDisplayOrder(int displayOrder) {
+ mDisplayOrder = displayOrder;
+ if (mAdapter != null) {
+ mAdapter.setContactNameDisplayOrder(displayOrder);
+ }
+ }
+
+ public int getSortOrder() {
+ return mSortOrder;
+ }
+
+ public void setSortOrder(int sortOrder) {
+ mSortOrder = sortOrder;
+ if (mAdapter != null) {
+ mAdapter.setSortOrder(sortOrder);
+ }
+ }
+
+ public void setContextMenuAdapter(ContextMenuAdapter adapter) {
+ mContextMenuAdapter = adapter;
+ if (mListView != null) {
+ mListView.setOnCreateContextMenuListener(adapter);
+ }
+ }
+
+ public ContextMenuAdapter getContextMenuAdapter() {
+ return mContextMenuAdapter;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ configurePhotoLoader();
+ }
+
+ protected void loadPreferences(ContactsPreferences contactsPrefs) {
+ setContactNameDisplayOrder(contactsPrefs.getDisplayOrder());
+ setSortOrder(contactsPrefs.getSortOrder());
+ }
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+ // Retrieve list state. This will be applied in onLoadFinished
+ if (savedState != null) {
+ mListState = savedState.getParcelable(LIST_STATE_KEY);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ onCreateView(inflater, container);
+
+ mAdapter = createListAdapter();
+ mAdapter.setSearchMode(isSearchMode());
+ mAdapter.setSearchResultsMode(isSearchResultsMode());
+ mAdapter.setPhotoLoader(mPhotoLoader);
+ mListView.setAdapter(mAdapter);
+
+ return mView;
+ }
+
+ protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+ mView = inflateView(inflater, container);
+
+ mListView = (ListView)mView.findViewById(android.R.id.list);
+ if (mListView == null) {
+ throw new RuntimeException(
+ "Your content must have a ListView whose id attribute is " +
+ "'android.R.id.list'");
+ }
+
+ View emptyView = mView.findViewById(com.android.internal.R.id.empty);
+ if (emptyView != null) {
+ mListView.setEmptyView(emptyView);
+ if (emptyView instanceof ContactListEmptyView) {
+ mEmptyView = (ContactListEmptyView)emptyView;
+ }
+ }
+
+ mListView.setOnItemClickListener(this);
+ mListView.setOnFocusChangeListener(this);
+ mListView.setOnTouchListener(this);
+ mListView.setFastScrollEnabled(!isSearchMode());
+
+ // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
+ // them when an A-Z headers is visible.
+ mListView.setDividerHeight(0);
+
+ // We manually save/restore the listview state
+ mListView.setSaveEnabled(false);
+
+ if (mContextMenuAdapter != null) {
+ mListView.setOnCreateContextMenuListener(mContextMenuAdapter);
+ }
+
+ mAizy = (ContactListAizyView) mView.findViewById(R.id.contacts_list_aizy);
+ mAizy.setListView(mListView);
+
+ configureAizy();
+ configurePhotoLoader();
+ configureSearchResultText();
+ }
+
+ protected void configurePhotoLoader() {
+ Activity activity = getActivity();
+ if (isPhotoLoaderEnabled() && activity != null) {
+ if (mPhotoLoader == null) {
+ mPhotoLoader = new ContactPhotoLoader(activity, R.drawable.ic_contact_list_picture);
+ }
+ if (mListView != null) {
+ mListView.setOnScrollListener(this);
+ }
+ if (mAdapter != null) {
+ mAdapter.setPhotoLoader(mPhotoLoader);
+ }
+ }
+ }
+
+ protected void configureSearchResultText() {
+ if (isSearchResultsMode() && mView != null) {
+ TextView titleText = (TextView)mView.findViewById(R.id.search_results_for);
+ if (titleText != null) {
+ titleText.setText(Html.fromHtml(getActivity().getString(R.string.search_results_for,
+ "<b>" + getQueryString() + "</b>")));
+ }
+ }
+ }
+
+ protected void configureAdapter() {
+ if (mAdapter == null) {
+ return;
+ }
+
+ mAdapter.setQueryString(mQueryString);
+ mAdapter.setPinnedPartitionHeadersEnabled(mSearchMode);
+ mAdapter.setContactNameDisplayOrder(mDisplayOrder);
+ mAdapter.setSortOrder(mSortOrder);
+ mAdapter.setNameHighlightingEnabled(isNameHighlighingEnabled());
+ mAdapter.setSectionHeaderDisplayEnabled(mSectionHeaderDisplayEnabled);
+ }
+
+ protected boolean isNameHighlighingEnabled() {
+ // When sort order and display order contradict each other, we want to
+ // highlight the part of the name used for sorting.
+ if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY &&
+ mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE) {
+ return true;
+ } else if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE &&
+ mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+ if (isAizyEnabled()) {
+ mAizy.listOnScroll(firstVisibleItem);
+ }
+ }
+
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
+ mPhotoLoader.pause();
+ } else if (isPhotoLoaderEnabled()) {
+ mPhotoLoader.resume();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ registerProviderStatusObserver();
+
+ if (isPhotoLoaderEnabled()) {
+ mPhotoLoader.resume();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ if (isPhotoLoaderEnabled()) {
+ mPhotoLoader.stop();
+ }
+ super.onDestroy();
+ }
+
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ hideSoftKeyboard();
+
+ int adjPosition = position - mListView.getHeaderViewsCount();
+ if (adjPosition >= 0) {
+ onItemClick(adjPosition, id);
+ }
+ }
+
+ private void hideSoftKeyboard() {
+ // Hide soft keyboard, if visible
+ InputMethodManager inputMethodManager = (InputMethodManager)
+ getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0);
+ }
+
+ /**
+ * Dismisses the soft keyboard when the list takes focus.
+ */
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (view == mListView && hasFocus) {
+ hideSoftKeyboard();
+ }
+ }
+
+ /**
+ * Dismisses the soft keyboard when the list is touched.
+ */
+ public boolean onTouch(View view, MotionEvent event) {
+ if (view == mListView) {
+ hideSoftKeyboard();
+ }
+ return false;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ unregisterProviderStatusObserver();
+ }
+
+ /**
+ * Dismisses the search UI along with the keyboard if the filter text is empty.
+ */
+ public void onClose() {
+ hideSoftKeyboard();
+ finish();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle icicle) {
+ super.onSaveInstanceState(icicle);
+ if (mListView != null) {
+ mListState = mListView.onSaveInstanceState();
+ icicle.putParcelable(LIST_STATE_KEY, mListState);
+ }
+ }
+
+ /**
+ * Restore the list state after the adapter is populated.
+ */
+ private void completeRestoreInstanceState() {
+ if (mListState != null) {
+ mListView.onRestoreInstanceState(mListState);
+ mListState = null;
+ }
+ }
+
+ private ContentObserver mProviderStatusObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ checkProviderStatus(true);
+ }
+ };
+
+ /**
+ * Register an observer for provider status changes - we will need to
+ * reflect them in the UI.
+ */
+ private void registerProviderStatusObserver() {
+ getActivity().getContentResolver().registerContentObserver(ProviderStatus.CONTENT_URI,
+ false, mProviderStatusObserver);
+ }
+
+ /**
+ * Register an observer for provider status changes - we will need to
+ * reflect them in the UI.
+ */
+ private void unregisterProviderStatusObserver() {
+ getActivity().getContentResolver().unregisterContentObserver(mProviderStatusObserver);
+ }
+
+ /**
+ * Obtains the contacts provider status and configures the UI accordingly.
+ *
+ * @param loadData true if the method needs to start a query when the
+ * provider is in the normal state
+ * @return true if the provider status is normal
+ */
+ private boolean checkProviderStatus(boolean loadData) {
+ View importFailureView = findViewById(R.id.import_failure);
+ if (importFailureView == null) {
+ return true;
+ }
+
+ // This query can be performed on the UI thread because
+ // the API explicitly allows such use.
+ Cursor cursor = getActivity().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) {
+ reloadData();
+ }
+ break;
+
+ case ProviderStatus.STATUS_CHANGING_LOCALE:
+ setEmptyText(R.string.locale_change_in_progress);
+ mAdapter.changeCursor(null);
+ mAdapter.notifyDataSetInvalidated();
+ break;
+
+ case ProviderStatus.STATUS_UPGRADING:
+ setEmptyText(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 = getActivity().getResources().getString(
+ R.string.upgrade_out_of_memory, new Object[] {size});
+ TextView messageView = (TextView) findViewById(R.id.emptyText);
+ messageView.setText(message);
+ messageView.setVisibility(View.VISIBLE);
+ configureImportFailureView(importFailureView);
+ mAdapter.changeCursor(null);
+ mAdapter.notifyDataSetInvalidated();
+ break;
+ }
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ importFailureView.setVisibility(
+ mProviderStatus == ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY
+ ? View.VISIBLE
+ : View.GONE);
+ return mProviderStatus == ProviderStatus.STATUS_NORMAL;
+ }
+
+ private void configureImportFailureView(View importFailureView) {
+
+ OnClickListener listener = new OnClickListener(){
+
+ public void onClick(View v) {
+ switch(v.getId()) {
+ case R.id.import_failure_uninstall_apps: {
+ // TODO break into a separate method
+ getActivity().startActivity(
+ new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
+ break;
+ }
+ case R.id.import_failure_retry_upgrade: {
+ // Send a provider status update, which will trigger a retry
+ ContentValues values = new ContentValues();
+ values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING);
+ getActivity().getContentResolver().update(ProviderStatus.CONTENT_URI,
+ values, null, null);
+ break;
+ }
+ }
+ }};
+
+ Button uninstallApps = (Button) findViewById(R.id.import_failure_uninstall_apps);
+ uninstallApps.setOnClickListener(listener);
+
+ Button retryUpgrade = (Button) findViewById(R.id.import_failure_retry_upgrade);
+ retryUpgrade.setOnClickListener(listener);
+ }
+
+ private View findViewById(int id) {
+ return mView.findViewById(id);
+ }
+
+ // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
+ public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
+ if (count == 0) {
+ return getActivity().getString(zeroResourceId);
+ } else {
+ String format = getActivity().getResources()
+ .getQuantityText(pluralResourceId, count).toString();
+ return String.format(format, count);
+ }
+ }
+
+ protected void setEmptyText(int resourceId) {
+ TextView empty = (TextView) getEmptyView().findViewById(R.id.emptyText);
+ empty.setText(getActivity().getText(resourceId));
+ empty.setVisibility(View.VISIBLE);
+ }
+
+ // TODO redesign into an async task or loader
+ protected boolean isSyncActive() {
+ Account[] accounts = AccountManager.get(getActivity()).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;
+ }
+
+ protected boolean hasIccCard() {
+ TelephonyManager telephonyManager =
+ (TelephonyManager)getActivity().getSystemService(Context.TELEPHONY_SERVICE);
+ return telephonyManager.hasIccCard();
+ }
+
+ /**
+ * Processes a user request to start search. This may be triggered by the
+ * search key, a menu item or some other user action.
+ */
+ public void startSearch(String initialQuery) {
+ ContactsSearchManager.startSearch(getActivity(), initialQuery, mRequest);
+ }
+
+ // TODO integrate into picker fragments
+// protected Uri buildCallingPackageUri(Uri uri) {
+// String callingPackage = getContext().getCallingPackage();
+// if (!TextUtils.isEmpty(callingPackage)) {
+// uri = uri.buildUpon().appendQueryParameter(
+// ContactsContract.REQUESTING_PACKAGE_PARAM_KEY, callingPackage).build();
+// }
+// }
+}
diff --git a/src/com/android/contacts/list/ContactListAdapter.java b/src/com/android/contacts/list/ContactListAdapter.java
new file mode 100644
index 0000000..4c602c8
--- /dev/null
+++ b/src/com/android/contacts/list/ContactListAdapter.java
@@ -0,0 +1,213 @@
+/*
+ * 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.list;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.SearchSnippetColumns;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import android.widget.QuickContactBadge;
+
+/**
+ * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
+ */
+public abstract class ContactListAdapter extends ContactEntryListAdapter {
+
+ protected static final String[] PROJECTION = new String[] {
+ Contacts._ID, // 0
+ Contacts.DISPLAY_NAME_PRIMARY, // 1
+ Contacts.DISPLAY_NAME_ALTERNATIVE, // 2
+ Contacts.SORT_KEY_PRIMARY, // 3
+ Contacts.STARRED, // 4
+ Contacts.CONTACT_PRESENCE, // 5
+ Contacts.PHOTO_ID, // 6
+ Contacts.LOOKUP_KEY, // 7
+ Contacts.PHONETIC_NAME, // 8
+ Contacts.HAS_PHONE_NUMBER, // 9
+ };
+
+ protected static final String[] FILTER_PROJECTION = new String[] {
+ Contacts._ID, // 0
+ Contacts.DISPLAY_NAME_PRIMARY, // 1
+ Contacts.DISPLAY_NAME_ALTERNATIVE, // 2
+ Contacts.SORT_KEY_PRIMARY, // 3
+ Contacts.STARRED, // 4
+ Contacts.CONTACT_PRESENCE, // 5
+ Contacts.PHOTO_ID, // 6
+ Contacts.LOOKUP_KEY, // 7
+ Contacts.PHONETIC_NAME, // 8
+ Contacts.HAS_PHONE_NUMBER, // 9
+ SearchSnippetColumns.SNIPPET_MIMETYPE, // 10
+ SearchSnippetColumns.SNIPPET_DATA1, // 11
+ SearchSnippetColumns.SNIPPET_DATA4, // 12
+ };
+
+ protected static final int CONTACT_ID_COLUMN_INDEX = 0;
+ protected static final int CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1;
+ protected static final int CONTACT_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2;
+ protected static final int CONTACT_SORT_KEY_PRIMARY_COLUMN_INDEX = 3;
+ protected static final int CONTACT_STARRED_COLUMN_INDEX = 4;
+ protected static final int CONTACT_PRESENCE_STATUS_COLUMN_INDEX = 5;
+ protected static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 6;
+ protected static final int CONTACT_LOOKUP_KEY_COLUMN_INDEX = 7;
+ protected static final int CONTACT_PHONETIC_NAME_COLUMN_INDEX = 8;
+ protected static final int CONTACT_HAS_PHONE_COLUMN_INDEX = 9;
+ protected static final int CONTACT_SNIPPET_MIMETYPE_COLUMN_INDEX = 10;
+ protected static final int CONTACT_SNIPPET_DATA1_COLUMN_INDEX = 11;
+ protected static final int CONTACT_SNIPPET_DATA4_COLUMN_INDEX = 12;
+
+ private boolean mQuickContactEnabled;
+ private CharSequence mUnknownNameText;
+ private int mDisplayNameColumnIndex;
+ private int mAlternativeDisplayNameColumnIndex;
+
+ public ContactListAdapter(Context context) {
+ super(context);
+
+ mUnknownNameText = context.getText(android.R.string.unknownName);
+ }
+
+ public CharSequence getUnknownNameText() {
+ return mUnknownNameText;
+ }
+
+ protected static Uri buildSectionIndexerUri(Uri uri) {
+ return uri.buildUpon()
+ .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
+ }
+
+ public boolean getHasPhoneNumber(int position) {
+ return ((Cursor)getItem(position)).getInt(CONTACT_HAS_PHONE_COLUMN_INDEX) != 0;
+ }
+
+ public boolean isContactStarred(int position) {
+ return ((Cursor)getItem(position)).getInt(CONTACT_STARRED_COLUMN_INDEX) != 0;
+ }
+
+ @Override
+ public String getContactDisplayName(int position) {
+ return ((Cursor)getItem(position)).getString(mDisplayNameColumnIndex);
+ }
+
+ public boolean isQuickContactEnabled() {
+ return mQuickContactEnabled;
+ }
+
+ public void setQuickContactEnabled(boolean quickContactEnabled) {
+ mQuickContactEnabled = quickContactEnabled;
+ }
+
+ @Override
+ public void setContactNameDisplayOrder(int displayOrder) {
+ super.setContactNameDisplayOrder(displayOrder);
+ if (getContactNameDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+ mDisplayNameColumnIndex = CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
+ mAlternativeDisplayNameColumnIndex = CONTACT_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
+ } else {
+ mDisplayNameColumnIndex = CONTACT_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
+ mAlternativeDisplayNameColumnIndex = CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
+ }
+ }
+
+ /**
+ * Builds the {@link Contacts#CONTENT_LOOKUP_URI} for the given
+ * {@link ListView} position.
+ */
+ public Uri getContactUri(int position) {
+ Cursor item = (Cursor)getItem(position);
+ return item != null ? getContactUri(item) : null;
+ }
+
+ public Uri getContactUri(Cursor cursor) {
+ long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX);
+ String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
+ return Contacts.getLookupUri(contactId, lookupKey);
+ }
+
+ @Override
+ protected View newView(Context context, int partition, Cursor cursor, int position,
+ ViewGroup parent) {
+ final ContactListItemView view = new ContactListItemView(context, null);
+ view.setUnknownNameText(mUnknownNameText);
+ view.setTextWithHighlightingFactory(getTextWithHighlightingFactory());
+ return view;
+ }
+
+ protected void bindSectionHeaderAndDivider(ContactListItemView view, int position) {
+ if (isSectionHeaderDisplayEnabled()) {
+ final int section = getSectionForPosition(position);
+ if (section != -1 && getPositionForSection(section) == position) {
+ String title = (String)getSections()[section];
+ view.setSectionHeader(title);
+ } else {
+ view.setDividerVisible(false);
+ view.setSectionHeader(null);
+ }
+
+ // move the divider for the last item in a section
+ if (getPositionForSection(section + 1) - 1 == position) {
+ view.setDividerVisible(false);
+ } else {
+ view.setDividerVisible(true);
+ }
+ } else {
+ view.setSectionHeader(null);
+ view.setDividerVisible(true);
+ }
+ }
+
+ protected void bindPhoto(final ContactListItemView view, Cursor cursor) {
+ // Set the photo, if available
+ long photoId = 0;
+ if (!cursor.isNull(CONTACT_PHOTO_ID_COLUMN_INDEX)) {
+ photoId = cursor.getLong(CONTACT_PHOTO_ID_COLUMN_INDEX);
+ }
+
+ getPhotoLoader().loadPhoto(view.getPhotoView(), photoId);
+ }
+
+ protected void bindQuickContact(final ContactListItemView view, Cursor cursor) {
+ long photoId = 0;
+ if (!cursor.isNull(CONTACT_PHOTO_ID_COLUMN_INDEX)) {
+ photoId = cursor.getLong(CONTACT_PHOTO_ID_COLUMN_INDEX);
+ }
+
+ QuickContactBadge quickContact = view.getQuickContact();
+ quickContact.assignContactUri(getContactUri(cursor));
+ getPhotoLoader().loadPhoto(quickContact, photoId);
+ }
+
+ protected void bindName(final ContactListItemView view, Cursor cursor) {
+ view.showDisplayName(cursor, mDisplayNameColumnIndex, isNameHighlightingEnabled(),
+ mAlternativeDisplayNameColumnIndex);
+ view.showPhoneticName(cursor, CONTACT_PHONETIC_NAME_COLUMN_INDEX);
+ }
+
+ protected void bindPresence(final ContactListItemView view, Cursor cursor) {
+ view.showPresence(cursor, CONTACT_PRESENCE_STATUS_COLUMN_INDEX);
+ }
+
+ protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
+ view.showSnippet(cursor, CONTACT_SNIPPET_MIMETYPE_COLUMN_INDEX,
+ CONTACT_SNIPPET_DATA1_COLUMN_INDEX, CONTACT_SNIPPET_DATA4_COLUMN_INDEX);
+ }
+}
diff --git a/src/com/android/contacts/list/ContactListAizyView.java b/src/com/android/contacts/list/ContactListAizyView.java
new file mode 100644
index 0000000..07ce12c
--- /dev/null
+++ b/src/com/android/contacts/list/ContactListAizyView.java
@@ -0,0 +1,223 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+
+/**
+ * A View that displays the sections given by an Indexer and their relative sizes. For
+ * English and similar languages, this is an A to Z list (where only the used letters are
+ * displayed). As the sections are shown in their relative sizes, this View can be used as a
+ * scrollbar.
+ */
+public class ContactListAizyView extends View {
+ private static final String TAG = "ContactListAizyView";
+
+ // TODO: Put these into resource files or create from image resources
+ private static final int TEXT_WIDTH = 20;
+ private static final int CIRCLE_DIAMETER = 30;
+ private static final int PREVIEW_WIDTH = 130;
+ private static final int PREVIEW_HEIGHT = 115;
+
+ private SectionIndexer mIndexer;
+
+ private boolean mCalculateYCoordinates;
+ private ListView mListView;
+ private float mPosition;
+ private float mFactor;
+ private PopupWindow mPreviewPopupWindow;
+ private TextView mPreviewPopupTextView;
+ private boolean mPreviewPopupVisible;
+ private int[] mWindowOffset;
+ private float[] yPositions = null;
+
+ public ContactListAizyView(Context context) {
+ super(context);
+ }
+
+ public ContactListAizyView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public ContactListAizyView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ final LayoutInflater inflater =
+ (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mPreviewPopupWindow = new PopupWindow(
+ inflater.inflate(R.layout.aizy_popup_window, null, false),
+ PREVIEW_WIDTH, PREVIEW_HEIGHT);
+ mPreviewPopupTextView =
+ (TextView) mPreviewPopupWindow.getContentView().findViewById(R.id.caption);
+ }
+
+ public void setIndexer(SectionIndexer indexer) {
+ mIndexer = indexer;
+ mCalculateYCoordinates = true;
+ }
+
+ public void setListView(ListView listView) {
+ mListView = listView;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(TEXT_WIDTH + CIRCLE_DIAMETER, resolveSize(0, heightMeasureSpec));
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mCalculateYCoordinates = true;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mIndexer == null) return;
+
+ calcYCoordinates();
+
+ drawLineAndText(canvas);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mWindowOffset == null) {
+ mWindowOffset = new int[2];
+ getLocationInWindow(mWindowOffset);
+ }
+
+ final int previewX = mWindowOffset[0] + getWidth();
+ final int previewY = (int) event.getY() + mWindowOffset[1]
+ - mPreviewPopupWindow.getHeight() / 2;
+ final boolean previewPopupVisible = event.getActionMasked() == MotionEvent.ACTION_MOVE;
+ if (previewPopupVisible != mPreviewPopupVisible) {
+ if (previewPopupVisible) {
+ mPreviewPopupWindow.showAtLocation(this, Gravity.LEFT | Gravity.TOP,
+ previewX, previewY);
+ } else {
+ mPreviewPopupWindow.dismiss();
+ }
+ mPreviewPopupVisible = previewPopupVisible;
+ } else {
+ mPreviewPopupWindow.update(previewX, previewY, -1, -1);
+ }
+ final int position = Math.max(0, (int) (event.getY() / mFactor));
+ if (mIndexer != null) {
+ final int index = mIndexer.getSectionForPosition(position);
+ final Object[] sections = mIndexer.getSections();
+ final String caption =
+ (index != -1 && index < sections.length) ? sections[index].toString() : "";
+ mPreviewPopupTextView.setText(caption);
+ }
+ if (mListView != null) {
+ mListView.setSelectionFromTop(position, 0);
+ }
+
+ super.onTouchEvent(event);
+ return true;
+ }
+
+ private void calcYCoordinates() {
+ if (!mCalculateYCoordinates) return;
+ mCalculateYCoordinates = false;
+
+ // Get a String[] of the sections.
+ final Object[] sectionObjects = mIndexer.getSections();
+ final int sectionCount = sectionObjects.length;
+ final String[] sections;
+ if (sectionObjects instanceof String[]) {
+ sections = (String[]) sectionObjects;
+ } else {
+ sections = new String[sectionCount];
+ for (int i = 0; i < sectionCount; i++) {
+ sections[i] = sectionObjects[i] == null ? null : sectionObjects[i].toString();
+ }
+ }
+
+ mFactor = (float) getHeight() / mListView.getCount();
+ }
+
+ private void drawLineAndText(Canvas canvas) {
+ // TODO: Figure out how to set the text size and fetch the height in pixels. This
+ // behaviour is OK for prototypes, but has to be refined later
+ final float textSize = 20.0f;
+
+ // Move A down, Z up
+ final Paint paint = new Paint();
+ paint.setColor(Color.LTGRAY);
+ paint.setTextSize(textSize);
+ paint.setAntiAlias(true);
+ final Object[] sections = mIndexer.getSections();
+ canvas.drawLine(
+ TEXT_WIDTH + CIRCLE_DIAMETER * 0.5f, 0.0f,
+ TEXT_WIDTH + CIRCLE_DIAMETER * 0.5f, getHeight(),
+ paint);
+ final int sectionCount = sections.length;
+ if (yPositions == null || yPositions.length != sectionCount) {
+ yPositions = new float[sectionCount];
+ }
+
+ // Calculate Positions
+ for (int i = 0; i < sectionCount; i++) {
+ yPositions[i] = mIndexer.getPositionForSection(i) * mFactor;
+ }
+
+ // Draw
+ float lastVisibleY = Float.MAX_VALUE;
+ for (int i = sectionCount - 1; i >= 0; i--) {
+ final float y = yPositions[i];
+ if (lastVisibleY - textSize > y) {
+ canvas.drawText(sections[i].toString(), 0.0f, y + 0.5f * textSize, paint);
+ lastVisibleY = y;
+ }
+ canvas.drawLine(
+ TEXT_WIDTH + CIRCLE_DIAMETER * 0.5f - 2, y,
+ TEXT_WIDTH + CIRCLE_DIAMETER * 0.5f + 2, y,
+ paint);
+ }
+
+ paint.setColor(Color.YELLOW);
+ canvas.drawLine(
+ TEXT_WIDTH + CIRCLE_DIAMETER * 0.0f, mPosition * mFactor,
+ TEXT_WIDTH + CIRCLE_DIAMETER * 1.0f, mPosition * mFactor,
+ paint);
+ }
+
+ public void listOnScroll(int firstVisibleItem) {
+ mPosition = firstVisibleItem;
+ invalidate();
+ }
+}
diff --git a/src/com/android/contacts/ContactListItemView.java b/src/com/android/contacts/list/ContactListItemView.java
similarity index 67%
rename from src/com/android/contacts/ContactListItemView.java
rename to src/com/android/contacts/list/ContactListItemView.java
index 89e4265..28444a4 100644
--- a/src/com/android/contacts/ContactListItemView.java
+++ b/src/com/android/contacts/list/ContactListItemView.java
@@ -14,17 +14,25 @@
* limitations under the License.
*/
-package com.android.contacts;
+package com.android.contacts.list;
-import com.android.contacts.ui.widget.DontPressWithParentImageView;
+import com.android.contacts.ContactPresenceIconUtil;
+import com.android.contacts.R;
+import com.android.contacts.widget.TextWithHighlighting;
+import com.android.contacts.widget.TextWithHighlightingFactory;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.database.CharArrayBuffer;
+import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
@@ -44,7 +52,7 @@
private static final int QUICK_CONTACT_BADGE_STYLE =
com.android.internal.R.attr.quickContactBadgeStyleWindowMedium;
- private final Context mContext;
+ protected final Context mContext;
private final int mPreferredHeight;
private final int mVerticalDividerMargin;
@@ -58,7 +66,7 @@
private final int mPresenceIconMargin;
private final int mHeaderTextWidth;
- private boolean mHorizontalDividerVisible;
+ private boolean mHorizontalDividerVisible = true;
private Drawable mHorizontalDividerDrawable;
private int mHorizontalDividerHeight;
@@ -74,6 +82,7 @@
private QuickContactBadge mQuickContact;
private ImageView mPhotoView;
private TextView mNameTextView;
+ private TextView mPhoneticNameTextView;
private DontPressWithParentImageView mCallButton;
private TextView mLabelView;
private TextView mDataView;
@@ -85,8 +94,38 @@
private int mLine1Height;
private int mLine2Height;
private int mLine3Height;
+ private int mLine4Height;
private OnClickListener mCallButtonClickListener;
+ private TextWithHighlightingFactory mTextWithHighlightingFactory;
+ 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);
+
+ private CharSequence mUnknownNameText;
+
+ /**
+ * Special class to allow the parent to be pressed without being pressed itself.
+ * This way the line of a tab can be pressed, but the image itself is not.
+ */
+ // TODO: understand this
+ private static class DontPressWithParentImageView extends ImageView {
+
+ public DontPressWithParentImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void setPressed(boolean pressed) {
+ // If the parent is pressed, do not set to pressed.
+ if (pressed && ((View) getParent()).isPressed()) {
+ return;
+ }
+ super.setPressed(pressed);
+ }
+ }
public ContactListItemView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -128,6 +167,14 @@
mCallButtonClickListener = callButtonClickListener;
}
+ public void setTextWithHighlightingFactory(TextWithHighlightingFactory factory) {
+ mTextWithHighlightingFactory = factory;
+ }
+
+ public void setUnknownNameText(CharSequence unknownNameText) {
+ mUnknownNameText = unknownNameText;
+ }
+
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// We will match parent's width and wrap content vertically, but make sure
@@ -138,28 +185,36 @@
mLine1Height = 0;
mLine2Height = 0;
mLine3Height = 0;
+ mLine4Height = 0;
// Obtain the natural dimensions of the name text (we only care about height)
- mNameTextView.measure(0, 0);
+ if (isVisible(mNameTextView)) {
+ mNameTextView.measure(0, 0);
+ mLine1Height = mNameTextView.getMeasuredHeight();
+ }
- mLine1Height = mNameTextView.getMeasuredHeight();
+ if (isVisible(mPhoneticNameTextView)) {
+ mPhoneticNameTextView.measure(0, 0);
+ mLine2Height = mPhoneticNameTextView.getMeasuredHeight();
+ }
if (isVisible(mLabelView)) {
mLabelView.measure(0, 0);
- mLine2Height = mLabelView.getMeasuredHeight();
+ mLine3Height = mLabelView.getMeasuredHeight();
}
if (isVisible(mDataView)) {
mDataView.measure(0, 0);
- mLine2Height = Math.max(mLine2Height, mDataView.getMeasuredHeight());
+ mLine3Height = Math.max(mLine3Height, mDataView.getMeasuredHeight());
}
if (isVisible(mSnippetView)) {
mSnippetView.measure(0, 0);
- mLine3Height = mSnippetView.getMeasuredHeight();
+ mLine4Height = mSnippetView.getMeasuredHeight();
}
- height += mLine1Height + mLine2Height + mLine3Height;
+ height += mLine1Height + mLine2Height + mLine3Height + mLine4Height
+ + mPaddingTop + mPaddingBottom;
if (isVisible(mCallButton)) {
mCallButton.measure(0, 0);
@@ -178,7 +233,12 @@
mHeaderTextView.measure(
MeasureSpec.makeMeasureSpec(mHeaderTextWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
- height += mHeaderBackgroundDrawable.getIntrinsicHeight();
+ height += mHeaderBackgroundHeight;
+ }
+
+ if (mHorizontalDividerVisible) {
+ ensureHorizontalDivider();
+ height += mHorizontalDividerHeight;
}
setMeasuredDimension(width, height);
@@ -207,8 +267,73 @@
// by laying out the left and right sides. Then we will allocate the remainder
// to the text fields in the middle.
- // Left side
- int leftBound = mPaddingLeft;
+ int leftBound = layoutLeftSide(height, topBound, mPaddingLeft);
+ int rightBound = layoutRightSide(height, topBound, right);
+
+ if (mHorizontalDividerVisible) {
+ ensureHorizontalDivider();
+ mHorizontalDividerDrawable.setBounds(
+ 0,
+ height - mHorizontalDividerHeight,
+ width,
+ height);
+ }
+
+ topBound += mPaddingTop;
+ int bottomBound = height - mPaddingBottom;
+
+ // Text lines, centered vertically
+ rightBound -= mPaddingRight;
+
+ // Center text vertically
+ int totalTextHeight = mLine1Height + mLine2Height + mLine3Height + mLine4Height;
+ int textTopBound = (bottomBound + topBound - totalTextHeight) / 2;
+
+ if (isVisible(mNameTextView)) {
+ mNameTextView.layout(leftBound,
+ textTopBound,
+ rightBound,
+ textTopBound + mLine1Height);
+ }
+
+ int dataLeftBound = leftBound;
+ if (isVisible(mPhoneticNameTextView)) {
+ mPhoneticNameTextView.layout(leftBound,
+ textTopBound + mLine1Height,
+ rightBound,
+ textTopBound + mLine1Height + mLine2Height);
+ }
+
+ if (isVisible(mLabelView)) {
+ dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
+ mLabelView.layout(leftBound,
+ textTopBound + mLine1Height + mLine2Height,
+ dataLeftBound,
+ textTopBound + mLine1Height + mLine2Height + mLine3Height);
+ dataLeftBound += mGapBetweenLabelAndData;
+ }
+
+ if (isVisible(mDataView)) {
+ mDataView.layout(dataLeftBound,
+ textTopBound + mLine1Height + mLine2Height,
+ rightBound,
+ textTopBound + mLine1Height + mLine2Height + mLine3Height);
+ }
+
+ if (isVisible(mSnippetView)) {
+ mSnippetView.layout(leftBound,
+ textTopBound + mLine1Height + mLine2Height + mLine3Height,
+ rightBound,
+ textTopBound + mLine1Height + mLine2Height + mLine3Height + mLine4Height);
+ }
+ }
+
+ /**
+ * Performs layout of the left side of the view
+ *
+ * @return new left boundary
+ */
+ protected int layoutLeftSide(int height, int topBound, int leftBound) {
View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
if (photoView != null) {
// Center the photo vertically
@@ -220,9 +345,15 @@
photoTop + mPhotoViewHeight);
leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
}
+ return leftBound;
+ }
- // Right side
- int rightBound = right;
+ /**
+ * Performs layout of the right side of the view
+ *
+ * @return new right boundary
+ */
+ protected int layoutRightSide(int height, int topBound, int rightBound) {
if (isVisible(mCallButton)) {
int buttonWidth = mCallButton.getMeasuredWidth();
rightBound -= buttonWidth;
@@ -230,7 +361,7 @@
rightBound,
topBound,
rightBound + buttonWidth,
- height);
+ height - mHorizontalDividerHeight);
mVerticalDividerVisible = true;
ensureVerticalDivider();
rightBound -= mVerticalDividerWidth;
@@ -252,57 +383,10 @@
rightBound + iconWidth,
height);
}
-
- if (mHorizontalDividerVisible) {
- ensureHorizontalDivider();
- mHorizontalDividerDrawable.setBounds(
- 0,
- height - mHorizontalDividerHeight,
- width,
- height);
- }
-
- topBound += mPaddingTop;
- int bottomBound = height - mPaddingBottom;
-
- // Text lines, centered vertically
- rightBound -= mPaddingRight;
-
- // Center text vertically
- int totalTextHeight = mLine1Height + mLine2Height + mLine3Height;
- int textTopBound = (bottomBound + topBound - totalTextHeight) / 2;
-
- mNameTextView.layout(leftBound,
- textTopBound,
- rightBound,
- textTopBound + mLine1Height);
-
- int dataLeftBound = leftBound;
- if (isVisible(mLabelView)) {
- dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
- mLabelView.layout(leftBound,
- textTopBound + mLine1Height,
- dataLeftBound,
- textTopBound + mLine1Height + mLine2Height);
- dataLeftBound += mGapBetweenLabelAndData;
- }
-
- if (isVisible(mDataView)) {
- mDataView.layout(dataLeftBound,
- textTopBound + mLine1Height,
- rightBound,
- textTopBound + mLine1Height + mLine2Height);
- }
-
- if (isVisible(mSnippetView)) {
- mSnippetView.layout(leftBound,
- textTopBound + mLine1Height + mLine2Height,
- rightBound,
- textTopBound + mLine1Height + mLine2Height + mLine3Height);
- }
+ return rightBound;
}
- private boolean isVisible(View view) {
+ protected boolean isVisible(View view) {
return view != null && view.getVisibility() == View.VISIBLE;
}
@@ -430,6 +514,24 @@
}
/**
+ * Removes the photo view. Should not be needed once we start handling different
+ * types of views as different types of views from the List's perspective.
+ *
+ * @deprecated
+ */
+ @Deprecated
+ public void removePhotoView() {
+ if (mPhotoView != null) {
+ removeView(mPhotoView);
+ mPhotoView = null;
+ }
+ if (mQuickContact != null) {
+ removeView(mQuickContact);
+ mQuickContact = null;
+ }
+ }
+
+ /**
* Returns the text view for the contact name, creating it if necessary.
*/
public TextView getNameTextView() {
@@ -470,6 +572,36 @@
}
/**
+ * Adds or updates a text view for the phonetic name.
+ */
+ public void setPhoneticName(char[] text, int size) {
+ if (text == null || size == 0) {
+ if (mPhoneticNameTextView != null) {
+ mPhoneticNameTextView.setVisibility(View.GONE);
+ }
+ } else {
+ getPhoneticNameTextView();
+ mPhoneticNameTextView.setText(text, 0, size);
+ mPhoneticNameTextView.setVisibility(VISIBLE);
+ }
+ }
+
+ /**
+ * Returns the text view for the phonetic name, creating it if necessary.
+ */
+ public TextView getPhoneticNameTextView() {
+ if (mPhoneticNameTextView == null) {
+ mPhoneticNameTextView = new TextView(mContext);
+ mPhoneticNameTextView.setSingleLine(true);
+ mPhoneticNameTextView.setEllipsize(TruncateAt.MARQUEE);
+ mPhoneticNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+ mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD);
+ addView(mPhoneticNameTextView);
+ }
+ return mPhoneticNameTextView;
+ }
+
+ /**
* Adds or updates a text view for the data label.
*/
public void setLabel(CharSequence text) {
@@ -592,4 +724,98 @@
}
}
}
+
+ public void showDisplayName(Cursor cursor, int nameColumnIndex, boolean highlightingEnabled,
+ int alternativeNameColumnIndex) {
+ cursor.copyStringToBuffer(nameColumnIndex, nameBuffer);
+ TextView nameView = getNameTextView();
+ int size = nameBuffer.sizeCopied;
+ if (size != 0) {
+ if (highlightingEnabled) {
+ if (textWithHighlighting == null) {
+ textWithHighlighting =
+ mTextWithHighlightingFactory.createTextWithHighlighting();
+ }
+ cursor.copyStringToBuffer(alternativeNameColumnIndex, highlightedTextBuffer);
+ textWithHighlighting.setText(nameBuffer, highlightedTextBuffer);
+ nameView.setText(textWithHighlighting);
+ } else {
+ nameView.setText(nameBuffer.data, 0, size);
+ }
+ } else {
+ nameView.setText(mUnknownNameText);
+ }
+ }
+
+ public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
+ cursor.copyStringToBuffer(phoneticNameColumnIndex, phoneticNameBuffer);
+ int phoneticNameSize = phoneticNameBuffer.sizeCopied;
+ if (phoneticNameSize != 0) {
+ setPhoneticName(phoneticNameBuffer.data, phoneticNameSize);
+ } else {
+ setPhoneticName(null, 0);
+ }
+ }
+
+ /**
+ * Sets the proper icon (star or presence or nothing)
+ */
+ public void showPresence(Cursor cursor, int presenceColumnIndex) {
+ int serverStatus;
+ if (!cursor.isNull(presenceColumnIndex)) {
+ serverStatus = cursor.getInt(presenceColumnIndex);
+
+ // TODO consider caching these drawables
+ Drawable icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), serverStatus);
+ if (icon != null) {
+ setPresence(icon);
+ } else {
+ setPresence(null);
+ }
+ } else {
+ setPresence(null);
+ }
+ }
+
+ /**
+ * Shows search snippet.
+ */
+ public void showSnippet(Cursor cursor, int summarySnippetMimetypeColumnIndex,
+ int summarySnippetData1ColumnIndex, int summarySnippetData4ColumnIndex) {
+ String snippet = null;
+ String snippetMimeType = cursor.getString(summarySnippetMimetypeColumnIndex);
+ if (Email.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
+ String email = cursor.getString(summarySnippetData1ColumnIndex);
+ if (!TextUtils.isEmpty(email)) {
+ snippet = email;
+ }
+ } else if (Organization.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
+ String company = cursor.getString(summarySnippetData1ColumnIndex);
+ String title = cursor.getString(summarySnippetData4ColumnIndex);
+ if (!TextUtils.isEmpty(company)) {
+ if (!TextUtils.isEmpty(title)) {
+ snippet = company + " / " + title;
+ } else {
+ snippet = company;
+ }
+ } else if (!TextUtils.isEmpty(title)) {
+ snippet = title;
+ }
+ } else if (Nickname.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
+ String nickname = cursor.getString(summarySnippetData1ColumnIndex);
+ if (!TextUtils.isEmpty(nickname)) {
+ snippet = nickname;
+ }
+ }
+
+ setSnippet(snippet);
+ }
+
+ /**
+ * Shows data element (e.g. phone number).
+ */
+ public void showData(Cursor cursor, int dataColumnIndex) {
+ cursor.copyStringToBuffer(dataColumnIndex, dataBuffer);
+ setData(dataBuffer.data, dataBuffer.sizeCopied);
+ }
}
diff --git a/src/com/android/contacts/list/ContactPickerFragment.java b/src/com/android/contacts/list/ContactPickerFragment.java
new file mode 100644
index 0000000..c3a50a0
--- /dev/null
+++ b/src/com/android/contacts/list/ContactPickerFragment.java
@@ -0,0 +1,169 @@
+/*
+ * 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.list;
+
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+import com.android.contacts.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+
+/**
+ * Fragment for the contact list used for browsing contacts (as compared to
+ * picking a contact with one of the PICK or SHORTCUT intents).
+ */
+public class ContactPickerFragment extends ContactEntryListFragment<ContactEntryListAdapter>
+ implements OnShortcutIntentCreatedListener {
+
+ private OnContactPickerActionListener mListener;
+ private boolean mCreateContactEnabled;
+ private boolean mShortcutRequested;
+
+ public ContactPickerFragment() {
+ setPhotoLoaderEnabled(true);
+ setSectionHeaderDisplayEnabled(true);
+ setAizyEnabled(true);
+ }
+
+ public void setOnContactPickerActionListener(OnContactPickerActionListener listener) {
+ mListener = listener;
+ }
+
+ public boolean isCreateContactEnabled() {
+ return mCreateContactEnabled;
+ }
+
+ public void setCreateContactEnabled(boolean flag) {
+ this.mCreateContactEnabled = flag;
+ }
+
+ public boolean isShortcutRequested() {
+ return mShortcutRequested;
+ }
+
+ public void setShortcutRequested(boolean flag) {
+ mShortcutRequested = flag;
+ }
+
+ @Override
+ protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+ super.onCreateView(inflater, container);
+ if (mCreateContactEnabled) {
+ getListView().addHeaderView(inflater.inflate(R.layout.create_new_contact, null, false));
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (position == 0 && !isSearchMode() && mCreateContactEnabled) {
+ mListener.onCreateNewContactAction();
+ } else {
+ super.onItemClick(parent, view, position, id);
+ }
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ Uri uri;
+ if (isLegacyCompatibilityMode()) {
+ uri = ((LegacyContactListAdapter)getAdapter()).getPersonUri(position);
+ } else {
+ uri = ((ContactListAdapter)getAdapter()).getContactUri(position);
+ }
+ if (mShortcutRequested) {
+ ShortcutIntentBuilder builder = new ShortcutIntentBuilder(getActivity(), this);
+ builder.createContactShortcutIntent(uri);
+ } else {
+ mListener.onPickContactAction(uri);
+ }
+ }
+
+ @Override
+ protected ContactEntryListAdapter createListAdapter() {
+ if (!isLegacyCompatibilityMode()) {
+ ContactListAdapter adapter = new DefaultContactListAdapter(getActivity());
+ adapter.setSectionHeaderDisplayEnabled(true);
+ adapter.setDisplayPhotos(true);
+ adapter.setQuickContactEnabled(false);
+ return adapter;
+ } else {
+ LegacyContactListAdapter adapter = new LegacyContactListAdapter(getActivity());
+ adapter.setSectionHeaderDisplayEnabled(false);
+ adapter.setDisplayPhotos(false);
+ return adapter;
+ }
+ }
+
+ @Override
+ protected void configureAdapter() {
+ super.configureAdapter();
+
+ ContactEntryListAdapter adapter = getAdapter();
+ if (adapter instanceof DefaultContactListAdapter) {
+ ((DefaultContactListAdapter)adapter).setVisibleContactsOnly(true);
+ }
+
+ // If "Create new contact" is shown, don't display the empty list UI
+ adapter.setEmptyListEnabled(!isCreateContactEnabled());
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ return inflater.inflate(R.layout.contacts_list_content, null);
+ }
+
+ @Override
+ protected void prepareEmptyView() {
+ if (isSearchMode()) {
+ return;
+ } else if (isSearchResultsMode()) {
+ setEmptyText(R.string.noMatchingContacts);
+ } else if (isSyncActive()) {
+ if (mShortcutRequested) {
+ // Help text is the same no matter whether there is SIM or not.
+ setEmptyText(R.string.noContactsHelpTextWithSyncForCreateShortcut);
+ } else if (hasIccCard()) {
+ setEmptyText(R.string.noContactsHelpTextWithSync);
+ } else {
+ setEmptyText(R.string.noContactsNoSimHelpTextWithSync);
+ }
+ } else {
+ if (mShortcutRequested) {
+ // Help text is the same no matter whether there is SIM or not.
+ setEmptyText(R.string.noContactsHelpTextWithSyncForCreateShortcut);
+ } else if (hasIccCard()) {
+ setEmptyText(R.string.noContactsHelpText);
+ } else {
+ setEmptyText(R.string.noContactsNoSimHelpText);
+ }
+ }
+ }
+
+ public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
+ mListener.onShortcutIntentCreated(shortcutIntent);
+ }
+
+ @Override
+ public void startSearch(String initialQuery) {
+ ContactsSearchManager.startSearchForResult(getActivity(), initialQuery,
+ ACTIVITY_REQUEST_CODE_FILTER, getContactsRequest());
+ }
+}
diff --git a/src/com/android/contacts/list/ContactsIntentResolver.java b/src/com/android/contacts/list/ContactsIntentResolver.java
new file mode 100644
index 0000000..8388daa
--- /dev/null
+++ b/src/com/android/contacts/list/ContactsIntentResolver.java
@@ -0,0 +1,228 @@
+/*
+ * 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.list;
+
+import com.android.contacts.CallContactActivity;
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.People;
+import android.provider.Contacts.Phones;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Intents.UI;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * Parses a Contacts intent, extracting all relevant parts and packaging them
+ * as a {@link ContactsRequest} object.
+ */
+@SuppressWarnings("deprecation")
+public class ContactsIntentResolver {
+
+ private static final String TAG = "ContactsListActivity";
+
+ private final Activity mContext;
+
+ public ContactsIntentResolver(Activity context) {
+ this.mContext = context;
+ }
+
+ public ContactsRequest resolveIntent(Intent intent) {
+ ContactsRequest request = new ContactsRequest();
+ request.setDisplayOnlyVisible(true);
+
+ String action = intent.getAction();
+
+ Log.i(TAG, "Called with action: " + action);
+
+ if (UI.LIST_DEFAULT.equals(action) ) {
+ request.setActionCode(ContactsRequest.ACTION_DEFAULT);
+ request.setDisplayWithPhonesOnlyOption(
+ ContactsRequest.DISPLAY_ONLY_WITH_PHONES_PREFERENCE);
+ } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
+ request.setActionCode(ContactsRequest.ACTION_DEFAULT);
+ request.setDisplayWithPhonesOnlyOption(
+ ContactsRequest.DISPLAY_ONLY_WITH_PHONES_DISABLED);
+ request.setDisplayOnlyVisible(false);
+ } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
+ request.setActionCode(ContactsRequest.ACTION_DEFAULT);
+ request.setDisplayWithPhonesOnlyOption(
+ ContactsRequest.DISPLAY_ONLY_WITH_PHONES_ENABLED);
+ } else if (UI.LIST_STARRED_ACTION.equals(action)) {
+ request.setActionCode(ContactsRequest.ACTION_STARRED);
+ } else if (UI.LIST_FREQUENT_ACTION.equals(action)) {
+ request.setActionCode(ContactsRequest.ACTION_FREQUENT);
+ } else if (UI.LIST_STREQUENT_ACTION.equals(action)) {
+ request.setActionCode(ContactsRequest.ACTION_STREQUENT);
+ } else if (UI.LIST_GROUP_ACTION.equals(action)) {
+ request.setActionCode(ContactsRequest.ACTION_GROUP);
+ String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY);
+ if (!TextUtils.isEmpty(groupName)) {
+ request.setGroupName(groupName);
+ } else {
+ Log.e(TAG, "Intent missing a required extra: " + UI.GROUP_NAME_EXTRA_KEY);
+ request.setValid(false);
+ }
+ } else if (Intent.ACTION_PICK.equals(action)) {
+ final String resolvedType = intent.resolveType(mContext);
+ if (Contacts.CONTENT_TYPE.equals(resolvedType)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_CONTACT);
+ } else if (People.CONTENT_TYPE.equals(resolvedType)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_CONTACT);
+ request.setLegacyCompatibilityMode(true);
+ } else if (Phone.CONTENT_TYPE.equals(resolvedType)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_PHONE);
+ } else if (Phones.CONTENT_TYPE.equals(resolvedType)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_PHONE);
+ request.setLegacyCompatibilityMode(true);
+ } else if (StructuredPostal.CONTENT_TYPE.equals(resolvedType)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_POSTAL);
+ } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(resolvedType)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_POSTAL);
+ request.setLegacyCompatibilityMode(true);
+ }
+ } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
+ String component = intent.getComponent().getClassName();
+ if (component.equals("alias.DialShortcut")) {
+ request.setActionCode(ContactsRequest.ACTION_CREATE_SHORTCUT_CALL);
+ request.setActivityTitle(mContext.getString(R.string.callShortcutActivityTitle));
+ } else if (component.equals("alias.MessageShortcut")) {
+ request.setActionCode(ContactsRequest.ACTION_CREATE_SHORTCUT_SMS);
+ request.setActivityTitle(mContext.getString(R.string.messageShortcutActivityTitle));
+ } else {
+ request.setActionCode(ContactsRequest.ACTION_CREATE_SHORTCUT_CONTACT);
+ request.setActivityTitle(mContext.getString(R.string.shortcutActivityTitle));
+ }
+ } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
+ String type = intent.getType();
+ if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT);
+ } else if (Phone.CONTENT_ITEM_TYPE.equals(type)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_PHONE);
+ } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_PHONE);
+ request.setLegacyCompatibilityMode(true);
+ } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(type)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_POSTAL);
+ } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_POSTAL);
+ request.setLegacyCompatibilityMode(true);
+ } else if (People.CONTENT_ITEM_TYPE.equals(type)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT);
+ request.setLegacyCompatibilityMode(true);
+ }
+ } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) {
+ request.setActionCode(ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT);
+ } else if (Intent.ACTION_SEARCH.equals(action)) {
+ // See if the suggestion was clicked with a search action key (call button)
+ if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
+ String query = intent.getStringExtra(SearchManager.QUERY);
+ if (!TextUtils.isEmpty(query)) {
+ Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ Uri.fromParts("tel", query, null));
+ request.setRedirectIntent(newIntent);
+ }
+ } else {
+ request.setQueryString(intent.getStringExtra(SearchManager.QUERY));
+ request.setSearchResultsMode(true);
+ }
+ } else if (UI.FILTER_CONTACTS_ACTION.equals(action)) {
+ // 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
+ // context for the search queries.
+ request.setActionCode(ContactsRequest.ACTION_DEFAULT);
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ request.setQueryString(extras.getString(UI.FILTER_TEXT_EXTRA_KEY));
+
+ ContactsRequest originalRequest =
+ (ContactsRequest)extras.get(ContactsSearchManager.ORIGINAL_REQUEST_KEY);
+ if (originalRequest != null) {
+ request.copyFrom(originalRequest);
+ }
+ }
+
+ if (request == null) {
+ request = new ContactsRequest();
+ }
+
+ request.setSearchMode(true);
+// } else if (ACTION_SEARCH_INTERNAL.equals(action)) {
+// String originalAction = null;
+// Bundle extras = intent.getExtras();
+// if (extras != null) {
+// originalAction = extras.getString(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY);
+// }
+// mShortcutAction = intent.getStringExtra(SHORTCUT_ACTION_KEY);
+//
+// if (Intent.ACTION_INSERT_OR_EDIT.equals(originalAction)) {
+// request.setActionCode(ContactsRequest.MODE_QUERY_PICK_TO_EDIT;
+// mShowSearchSnippets = true;
+// mQueryString = intent.getStringExtra(SearchManager.QUERY);
+// } else if (mShortcutAction != null && intent.hasExtra(Insert.PHONE)) {
+// request.setActionCode(ContactsRequest.MODE_QUERY_PICK_PHONE;
+// mQueryMode = QUERY_MODE_TEL;
+// mQueryString = intent.getStringExtra(Insert.PHONE);
+// } else {
+// request.setActionCode(ContactsRequest.MODE_QUERY_PICK;
+// mQueryMode = QUERY_MODE_NONE;
+// mShowSearchSnippets = true;
+// mQueryString = intent.getStringExtra(SearchManager.QUERY);
+// }
+// mSearchResultsMode = true;
+ // Since this is the filter activity it receives all intents
+ // dispatched from the SearchManager for security reasons
+ // so we need to re-dispatch from here to the intended target.
+ } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) {
+ Uri data = intent.getData();
+ // See if the suggestion was clicked with a search action key (call button)
+ if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
+ Intent newIntent = new Intent(mContext, CallContactActivity.class);
+ newIntent.setData(data);
+ request.setRedirectIntent(newIntent);
+ } else {
+ request.setRedirectIntent(new Intent(Intent.ACTION_VIEW, data));
+ }
+ } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) {
+ request.setRedirectIntent(new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData()));
+ } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) {
+ // TODO actually support this in EditContactActivity.
+ String number = intent.getData().getSchemeSpecificPart();
+ Intent newIntent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
+ newIntent.putExtra(Intents.Insert.PHONE, number);
+ request.setRedirectIntent(newIntent);
+
+ }
+ // 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) {
+ request.setActivityTitle(title);
+ }
+
+ return request;
+ }
+}
diff --git a/src/com/android/contacts/list/ContactsRequest.java b/src/com/android/contacts/list/ContactsRequest.java
new file mode 100644
index 0000000..6f2e2e4
--- /dev/null
+++ b/src/com/android/contacts/list/ContactsRequest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.list;
+
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Parsed form of the intent sent to the Contacts application.
+ */
+public class ContactsRequest implements Parcelable {
+
+ /** Default mode: browse contacts */
+ public static final int ACTION_DEFAULT = 10;
+
+ /** Show contents of a specific group */
+ public static final int ACTION_GROUP = 20;
+
+ /** Show all starred contacts */
+ public static final int ACTION_STARRED = 30;
+
+ /** Show frequently contacted contacts */
+ public static final int ACTION_FREQUENT = 40;
+
+ /** Show starred and the frequent */
+ public static final int ACTION_STREQUENT = 50;
+
+ /** Show all contacts and pick them when clicking */
+ public static final int ACTION_PICK_CONTACT = 60;
+
+ /** Show all contacts as well as the option to create a new one */
+ public static final int ACTION_PICK_OR_CREATE_CONTACT = 70;
+
+ /** Show all contacts and pick them for edit when clicking, and allow creating a new contact */
+ public static final int ACTION_INSERT_OR_EDIT_CONTACT = 80;
+
+ /** Show all phone numbers and pick them when clicking */
+ public static final int ACTION_PICK_PHONE = 90;
+
+ /** Show all postal addresses and pick them when clicking */
+ public static final int ACTION_PICK_POSTAL = 100;
+
+ /** Show all contacts and create a shortcut for the picked contact */
+ public static final int ACTION_CREATE_SHORTCUT_CONTACT = 110;
+
+ /** Show all phone numbers and create a call shortcut for the picked number */
+ public static final int ACTION_CREATE_SHORTCUT_CALL = 120;
+
+ /** Show all phone numbers and create an SMS shortcut for the picked number */
+ public static final int ACTION_CREATE_SHORTCUT_SMS = 130;
+
+ private boolean mValid = true;
+ private int mActionCode = ACTION_DEFAULT;
+ private Intent mRedirectIntent;
+ private CharSequence mTitle;
+ private boolean mSearchMode;
+ private boolean mSearchResultsMode;
+ private String mQueryString;
+
+ public static final int DISPLAY_ONLY_WITH_PHONES_PREFERENCE = 0;
+ public static final int DISPLAY_ONLY_WITH_PHONES_ENABLED = 1;
+ public static final int DISPLAY_ONLY_WITH_PHONES_DISABLED = 2;
+
+ private int mDisplayOnlyWithPhones;
+ private boolean mDisplayOnlyVisible;
+ private String mGroupName;
+ private boolean mLegacyCompatibilityMode;
+
+ /**
+ * Copies all fields.
+ */
+ public void copyFrom(ContactsRequest request) {
+ mValid = request.mValid;
+ mActionCode = request.mActionCode;
+ mRedirectIntent = request.mRedirectIntent;
+ mTitle = request.mTitle;
+ mSearchMode = request.mSearchMode;
+ mSearchResultsMode = request.mSearchResultsMode;
+ mQueryString = request.mQueryString;
+ mDisplayOnlyWithPhones = request.mDisplayOnlyWithPhones;
+ mDisplayOnlyVisible = request.mDisplayOnlyVisible;
+ mGroupName = request.mGroupName;
+ mLegacyCompatibilityMode = request.mLegacyCompatibilityMode;
+ }
+
+ public static Parcelable.Creator<ContactsRequest> CREATOR = new Creator<ContactsRequest>() {
+
+ public ContactsRequest[] newArray(int size) {
+ return new ContactsRequest[size];
+ }
+
+ public ContactsRequest createFromParcel(Parcel source) {
+ ContactsRequest request = new ContactsRequest();
+ request.mValid = source.readInt() != 0;
+ request.mActionCode = source.readInt();
+ request.mRedirectIntent = source.readParcelable(this.getClass().getClassLoader());
+ request.mTitle = source.readCharSequence();
+ request.mSearchMode = source.readInt() != 0;
+ request.mSearchResultsMode = source.readInt() != 0;
+ request.mQueryString = source.readString();
+ request.mDisplayOnlyWithPhones = source.readInt();
+ request.mDisplayOnlyVisible = source.readInt() != 0;
+ request.mGroupName = source.readString();
+ request.mLegacyCompatibilityMode = source.readInt() != 0;
+ return request;
+ }
+ };
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mValid ? 1 : 0);
+ dest.writeInt(mActionCode);
+ dest.writeParcelable(mRedirectIntent, 0);
+ dest.writeCharSequence(mTitle);
+ dest.writeInt(mSearchMode ? 1 : 0);
+ dest.writeInt(mSearchResultsMode ? 1 : 0);
+ dest.writeString(mQueryString);
+ dest.writeInt(mDisplayOnlyWithPhones);
+ dest.writeInt(mDisplayOnlyVisible ? 1 : 0);
+ dest.writeString(mGroupName);
+ dest.writeInt(mLegacyCompatibilityMode ? 1 : 0);
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public boolean isValid() {
+ return mValid;
+ }
+
+ public void setValid(boolean flag) {
+ mValid = flag;
+ }
+
+ public Intent getRedirectIntent() {
+ return mRedirectIntent;
+ }
+
+ public void setRedirectIntent(Intent intent) {
+ mRedirectIntent = intent;
+ }
+
+ public void setActivityTitle(CharSequence title) {
+ mTitle = title;
+ }
+
+ public CharSequence getActivityTitle() {
+ return mTitle;
+ }
+
+ public int getActionCode() {
+ return mActionCode;
+ }
+
+ public void setActionCode(int actionCode) {
+ mActionCode = actionCode;
+ }
+
+ public boolean getDisplayOnlyVisible() {
+ return mDisplayOnlyVisible;
+ }
+
+ public void setDisplayOnlyVisible(boolean flag) {
+ mDisplayOnlyVisible = flag;
+ }
+
+ public int getDisplayWithPhonesOnlyOption() {
+ return mDisplayOnlyWithPhones;
+ }
+
+ public void setDisplayWithPhonesOnlyOption(int option) {
+ mDisplayOnlyWithPhones = option;
+ }
+
+ public boolean isSearchMode() {
+ return mSearchMode;
+ }
+
+ public void setSearchMode(boolean flag) {
+ mSearchMode = flag;
+ }
+
+ public boolean isSearchResultsMode() {
+ return mSearchResultsMode;
+ }
+
+ public void setSearchResultsMode(boolean flag) {
+ mSearchResultsMode = flag;
+ }
+
+ public String getQueryString() {
+ return mQueryString;
+ }
+
+ public void setQueryString(String string) {
+ mQueryString = string;
+ }
+
+ public String getGroupName() {
+ return mGroupName;
+ }
+
+ public void setGroupName(String groupName) {
+ mGroupName = groupName;
+ }
+
+ public boolean isLegacyCompatibilityMode() {
+ return mLegacyCompatibilityMode;
+ }
+
+ public void setLegacyCompatibilityMode(boolean flag) {
+ mLegacyCompatibilityMode = flag;
+ }
+}
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
new file mode 100644
index 0000000..24b48d3
--- /dev/null
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -0,0 +1,181 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+import com.android.contacts.ui.ContactsPreferencesActivity.Prefs;
+
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.preference.PreferenceManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+/**
+ * Fragment containing a contact list used for browsing (as compared to
+ * picking a contact with one of the PICK intents).
+ */
+public class DefaultContactBrowseListFragment extends ContactBrowseListFragment {
+
+ private boolean mEditMode;
+ private boolean mCreateContactEnabled;
+ private int mDisplayWithPhonesOnlyOption = ContactsRequest.DISPLAY_ONLY_WITH_PHONES_DISABLED;
+ private boolean mVisibleContactsRestrictionEnabled = true;
+ private View mHeaderView;
+
+ public DefaultContactBrowseListFragment() {
+ setPhotoLoaderEnabled(true);
+ setSectionHeaderDisplayEnabled(true);
+ setAizyEnabled(true);
+ }
+
+ @Override
+ protected void prepareEmptyView() {
+ if (isShowingContactsWithPhonesOnly()) {
+ setEmptyText(R.string.noContactsWithPhoneNumbers);
+ } else {
+ super.prepareEmptyView();
+ }
+ }
+
+ private boolean isShowingContactsWithPhonesOnly() {
+ switch (mDisplayWithPhonesOnlyOption) {
+ case ContactsRequest.DISPLAY_ONLY_WITH_PHONES_DISABLED:
+ return false;
+ case ContactsRequest.DISPLAY_ONLY_WITH_PHONES_ENABLED:
+ return true;
+ case ContactsRequest.DISPLAY_ONLY_WITH_PHONES_PREFERENCE:
+ SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(getActivity());
+ return prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
+ Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
+ }
+ return false;
+ }
+
+ public void setDisplayWithPhonesOnlyOption(int displayWithPhonesOnly) {
+ mDisplayWithPhonesOnlyOption = displayWithPhonesOnly;
+ configureAdapter();
+ }
+
+ public void setVisibleContactsRestrictionEnabled(boolean flag) {
+ mVisibleContactsRestrictionEnabled = flag;
+ configureAdapter();
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ ContactListAdapter adapter = getAdapter();
+ if (isEditMode()) {
+ if (position == 0 && !isSearchMode() && isCreateContactEnabled()) {
+ createNewContact();
+ } else {
+ editContact(adapter.getContactUri(position));
+ }
+ } else {
+ viewContact(adapter.getContactUri(position));
+ }
+ }
+
+ @Override
+ protected ContactListAdapter createListAdapter() {
+ DefaultContactListAdapter adapter = new DefaultContactListAdapter(getActivity());
+ adapter.setSectionHeaderDisplayEnabled(isSectionHeaderDisplayEnabled());
+ adapter.setDisplayPhotos(true);
+ adapter.setQuickContactEnabled(true);
+ return adapter;
+ }
+
+ @Override
+ protected void configureAdapter() {
+ super.configureAdapter();
+
+ DefaultContactListAdapter adapter = (DefaultContactListAdapter)getAdapter();
+ if (adapter != null) {
+ adapter.setContactsWithPhoneNumbersOnly(isShowingContactsWithPhonesOnly());
+ adapter.setVisibleContactsOnly(mVisibleContactsRestrictionEnabled);
+ }
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ if (isSearchResultsMode()) {
+ return inflater.inflate(R.layout.contacts_list_search_results, null);
+ } else {
+ return inflater.inflate(R.layout.contacts_list_content, null);
+ }
+ }
+
+ @Override
+ protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+ super.onCreateView(inflater, container);
+
+ // Putting the header view inside a container will allow us to make
+ // it invisible later. See checkHeaderViewVisibility()
+ FrameLayout headerContainer = new FrameLayout(inflater.getContext());
+ mHeaderView = inflater.inflate(R.layout.total_contacts, null, false);
+ headerContainer.addView(mHeaderView);
+ getListView().addHeaderView(headerContainer);
+ checkHeaderViewVisibility();
+ }
+
+ @Override
+ public void setSearchMode(boolean flag) {
+ super.setSearchMode(flag);
+ checkHeaderViewVisibility();
+ }
+
+ private void checkHeaderViewVisibility() {
+ if (mHeaderView != null) {
+ mHeaderView.setVisibility(isSearchMode() ? View.GONE : View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected void showCount(int partitionIndex, Cursor data) {
+ if (!isSearchMode() && data != null) {
+ int count = data.getCount();
+ // TODO
+ // if (contactsListActivity.mDisplayOnlyPhones) {
+ // text = contactsListActivity.getQuantityText(count,
+ // R.string.listTotalPhoneContactsZero,
+ // R.plurals.listTotalPhoneContacts);
+ TextView textView = (TextView)getView().findViewById(R.id.totalContactsText);
+ String text = getQuantityText(count, R.string.listTotalAllContactsZero,
+ R.plurals.listTotalAllContacts);
+ textView.setText(text);
+ }
+ }
+
+ public void setEditMode(boolean flag) {
+ mEditMode = flag;
+ }
+
+ public boolean isEditMode() {
+ return mEditMode;
+ }
+
+ public void setCreateContactEnabled(boolean flag) {
+ this.mCreateContactEnabled = flag;
+ }
+
+ public boolean isCreateContactEnabled() {
+ return mCreateContactEnabled;
+ }
+}
diff --git a/src/com/android/contacts/list/DefaultContactListAdapter.java b/src/com/android/contacts/list/DefaultContactListAdapter.java
new file mode 100644
index 0000000..6819a6a
--- /dev/null
+++ b/src/com/android/contacts/list/DefaultContactListAdapter.java
@@ -0,0 +1,116 @@
+/*
+ * 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.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.view.View;
+
+/**
+ * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
+ */
+public class DefaultContactListAdapter extends ContactListAdapter {
+
+ private boolean mContactsWithPhoneNumbersOnly;
+ private boolean mVisibleContactsOnly;
+
+ public DefaultContactListAdapter(Context context) {
+ super(context);
+ }
+
+ public void setContactsWithPhoneNumbersOnly(boolean flag) {
+ mContactsWithPhoneNumbersOnly = flag;
+ }
+
+ public void setVisibleContactsOnly(boolean flag) {
+ mVisibleContactsOnly = flag;
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ Uri uri;
+
+ if (isSearchMode()) {
+ String query = getQueryString();
+ Builder builder = Contacts.CONTENT_FILTER_URI.buildUpon();
+ if (TextUtils.isEmpty(query)) {
+ builder.appendPath("");
+ } else {
+ builder.appendPath(query); // Builder will encode the query
+ }
+
+ builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+ String.valueOf(directoryId));
+ uri = builder.build();
+ loader.setProjection(FILTER_PROJECTION);
+ } else {
+ uri = Contacts.CONTENT_URI;
+ loader.setProjection(PROJECTION);
+ }
+
+ if (directoryId == Directory.DEFAULT) {
+ if (mVisibleContactsOnly && mContactsWithPhoneNumbersOnly) {
+ loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1"
+ + " AND " + Contacts.HAS_PHONE_NUMBER + "=1");
+ } else if (mVisibleContactsOnly) {
+ loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1");
+ } else if (mContactsWithPhoneNumbersOnly) {
+ loader.setSelection(Contacts.HAS_PHONE_NUMBER + "=1");
+ }
+ if (isSectionHeaderDisplayEnabled()) {
+ uri = buildSectionIndexerUri(uri);
+ }
+ }
+
+ loader.setUri(uri);
+
+ String sortOrder;
+ if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
+ sortOrder = Contacts.SORT_KEY_PRIMARY;
+ } else {
+ sortOrder = Contacts.SORT_KEY_ALTERNATIVE;
+ }
+
+ loader.setSortOrder(sortOrder);
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ final ContactListItemView view = (ContactListItemView)itemView;
+
+ bindSectionHeaderAndDivider(view, position);
+
+ if (isQuickContactEnabled()) {
+ bindQuickContact(view, cursor);
+ } else {
+ bindPhoto(view, cursor);
+ }
+
+ bindName(view, cursor);
+ bindPresence(view, cursor);
+
+ if (isSearchMode() || isSearchResultsMode()) {
+ bindSearchSnippet(view, cursor);
+ }
+ }
+}
diff --git a/src/com/android/contacts/list/DirectoryPartition.java b/src/com/android/contacts/list/DirectoryPartition.java
new file mode 100644
index 0000000..d7cb9bc
--- /dev/null
+++ b/src/com/android/contacts/list/DirectoryPartition.java
@@ -0,0 +1,88 @@
+/*
+ * 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.list;
+
+import com.android.contacts.widget.CompositeCursorAdapter;
+
+import android.provider.ContactsContract.Directory;
+
+/**
+ * Model object for a {@link Directory} row.
+ */
+public final class DirectoryPartition extends CompositeCursorAdapter.Partition {
+ private long mDirectoryId;
+ private String mDirectoryType;
+ private String mDisplayName;
+ private boolean mLoading;
+ private boolean mPriorityDirectory;
+
+ public DirectoryPartition(boolean showIfEmpty, boolean hasHeader) {
+ super(showIfEmpty, hasHeader);
+ }
+
+ /**
+ * Directory ID, see {@link Directory}.
+ */
+ public long getDirectoryId() {
+ return mDirectoryId;
+ }
+
+ public void setDirectoryId(long directoryId) {
+ this.mDirectoryId = directoryId;
+ }
+
+ /**
+ * Directory type resolved from {@link Directory#PACKAGE_NAME} and
+ * {@link Directory#TYPE_RESOURCE_ID};
+ */
+ public String getDirectoryType() {
+ return mDirectoryType;
+ }
+
+ public void setDirectoryType(String directoryType) {
+ this.mDirectoryType = directoryType;
+ }
+
+ /**
+ * See {@link Directory#DISPLAY_NAME}.
+ */
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.mDisplayName = displayName;
+ }
+
+ public boolean isLoading() {
+ return mLoading;
+ }
+
+ public void setLoading(boolean loading) {
+ mLoading = loading;
+ }
+
+ /**
+ * Returns true if this directory should be loaded before non-priority directories.
+ */
+ public boolean isPriorityDirectory() {
+ return mPriorityDirectory;
+ }
+
+ public void setPriorityDirectory(boolean priorityDirectory) {
+ mPriorityDirectory = priorityDirectory;
+ }
+}
diff --git a/src/com/android/contacts/list/JoinContactListAdapter.java b/src/com/android/contacts/list/JoinContactListAdapter.java
new file mode 100644
index 0000000..6258be4
--- /dev/null
+++ b/src/com/android/contacts/list/JoinContactListAdapter.java
@@ -0,0 +1,221 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.content.CursorLoader;
+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.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+public class JoinContactListAdapter extends ContactListAdapter {
+
+ /** Maximum number of suggestions shown for joining aggregates */
+ private static final int MAX_SUGGESTIONS = 4;
+
+ public static final int PARTITION_SUGGESTIONS = 0;
+ public static final int PARTITION_SHOW_ALL_CONTACTS = 1;
+ public static final int PARTITION_ALL_CONTACTS = 2;
+
+ private long mTargetContactId;
+
+ private int mShowAllContactsViewType;
+
+ /**
+ * Determines whether we display a list item with the label
+ * "Show all contacts" or actually show all contacts
+ */
+ private boolean mAllContactsListShown;
+
+
+ public JoinContactListAdapter(Context context) {
+ super(context);
+ setPinnedPartitionHeadersEnabled(true);
+ setSectionHeaderDisplayEnabled(true);
+ setIndexedPartition(PARTITION_ALL_CONTACTS);
+ mShowAllContactsViewType = super.getViewTypeCount();
+ }
+
+ @Override
+ protected void addPartitions() {
+
+ // Partition 0: suggestions
+ addPartition(false, true);
+
+ // Partition 1: "Show all contacts"
+ addPartition(false, false);
+
+ // Partition 2: All contacts
+ addPartition(false, true);
+ }
+
+ public void setTargetContactId(long targetContactId) {
+ this.mTargetContactId = targetContactId;
+ }
+
+ @Override
+ public void configureLoader(CursorLoader cursorLoader, long directoryId) {
+ JoinContactLoader loader = (JoinContactLoader)cursorLoader;
+ loader.setLoadSuggestionsAndAllContacts(mAllContactsListShown);
+
+ Builder builder = Contacts.CONTENT_URI.buildUpon();
+ builder.appendEncodedPath(String.valueOf(mTargetContactId));
+ builder.appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY);
+
+ String filter = getQueryString();
+ if (!TextUtils.isEmpty(filter)) {
+ builder.appendEncodedPath(Uri.encode(filter));
+ }
+
+ builder.appendQueryParameter("limit", String.valueOf(MAX_SUGGESTIONS));
+
+ loader.setSuggestionUri(builder.build());
+
+ // TODO simplify projection
+ loader.setProjection(PROJECTION);
+ loader.setUri(buildSectionIndexerUri(Contacts.CONTENT_URI));
+ loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1 AND " + Contacts._ID + "!=?");
+ loader.setSelectionArgs(new String[]{String.valueOf(mTargetContactId)});
+ if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
+ loader.setSortOrder(Contacts.SORT_KEY_PRIMARY);
+ } else {
+ loader.setSortOrder(Contacts.SORT_KEY_ALTERNATIVE);
+ }
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ public boolean isAllContactsListShown() {
+ return mAllContactsListShown;
+ }
+
+ public void setAllContactsListShown(boolean flag) {
+ mAllContactsListShown = flag;
+ }
+
+ public void setSuggestionsCursor(Cursor cursor) {
+ changeCursor(PARTITION_SUGGESTIONS, cursor);
+ if (cursor != null && cursor.getCount() != 0 && !mAllContactsListShown) {
+ changeCursor(PARTITION_SHOW_ALL_CONTACTS, getShowAllContactsLabelCursor());
+ } else {
+ changeCursor(PARTITION_SHOW_ALL_CONTACTS, null);
+ }
+ }
+
+ @Override
+ public void changeCursor(Cursor cursor) {
+ changeCursor(PARTITION_ALL_CONTACTS, cursor);
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return super.getViewTypeCount() + 1;
+ }
+
+ @Override
+ protected int getItemViewType(int partition, int position) {
+ if (partition == PARTITION_SHOW_ALL_CONTACTS) {
+ return mShowAllContactsViewType;
+ }
+ return super.getItemViewType(partition, position);
+ }
+
+ @Override
+ protected View newHeaderView(Context context, int partition, Cursor cursor,
+ ViewGroup parent) {
+ switch (partition) {
+ case PARTITION_SUGGESTIONS: {
+ TextView view = (TextView) inflate(R.layout.list_separator, parent);
+ view.setText(R.string.separatorJoinAggregateSuggestions);
+ return view;
+ }
+ case PARTITION_ALL_CONTACTS: {
+ TextView view = (TextView) inflate(R.layout.list_separator, parent);
+ view.setText(R.string.separatorJoinAggregateAll);
+ return view;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected View newView(Context context, int partition, Cursor cursor, int position,
+ ViewGroup parent) {
+ switch (partition) {
+ case PARTITION_SUGGESTIONS:
+ case PARTITION_ALL_CONTACTS:
+ return super.newView(context, partition, cursor, position, parent);
+ case PARTITION_SHOW_ALL_CONTACTS:
+ return inflate(R.layout.contacts_list_show_all_item, parent);
+ }
+ return null;
+ }
+
+ private View inflate(int layoutId, ViewGroup parent) {
+ return LayoutInflater.from(getContext()).inflate(layoutId, parent, false);
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ switch (partition) {
+ case PARTITION_SUGGESTIONS: {
+ final ContactListItemView view = (ContactListItemView)itemView;
+ bindPhoto(view, cursor);
+ bindName(view, cursor);
+ break;
+ }
+ case PARTITION_SHOW_ALL_CONTACTS: {
+ break;
+ }
+ case PARTITION_ALL_CONTACTS: {
+ final ContactListItemView view = (ContactListItemView)itemView;
+ bindSectionHeaderAndDivider(view, position);
+ bindPhoto(view, cursor);
+ bindName(view, cursor);
+ break;
+ }
+ }
+ }
+
+ public Cursor getShowAllContactsLabelCursor() {
+ MatrixCursor matrixCursor = new MatrixCursor(PROJECTION);
+ Object[] row = new Object[PROJECTION.length];
+ matrixCursor.addRow(row);
+ return matrixCursor;
+ }
+
+ @Override
+ public Uri getContactUri(Cursor cursor) {
+ long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX);
+ String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
+ return Contacts.getLookupUri(contactId, lookupKey);
+ }
+}
diff --git a/src/com/android/contacts/list/JoinContactListFragment.java b/src/com/android/contacts/list/JoinContactListFragment.java
new file mode 100644
index 0000000..12b4ae4
--- /dev/null
+++ b/src/com/android/contacts/list/JoinContactListFragment.java
@@ -0,0 +1,124 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.app.Activity;
+import android.content.ContentUris;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/**
+ * Fragment for the Join Contact list.
+ */
+public class JoinContactListFragment extends ContactEntryListFragment<JoinContactListAdapter> {
+
+ private static final int DISPLAY_NAME_LOADER = 1;
+
+ private OnContactPickerActionListener mListener;
+ private long mTargetContactId;
+ private boolean mAllContactsListShown = false;
+
+ public JoinContactListFragment() {
+ setPhotoLoaderEnabled(true);
+ setSectionHeaderDisplayEnabled(true);
+ }
+
+ public void setOnContactPickerActionListener(OnContactPickerActionListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ protected void onInitializeLoaders() {
+ super.onInitializeLoaders();
+ startLoading(DISPLAY_NAME_LOADER, null);
+ }
+
+ @Override
+ protected Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ if (id == DISPLAY_NAME_LOADER) {
+ // Loader for the display name of the target contact
+ return new CursorLoader(getActivity(),
+ ContentUris.withAppendedId(Contacts.CONTENT_URI, mTargetContactId),
+ new String[] {Contacts.DISPLAY_NAME}, null, null, null);
+ } else {
+ return new JoinContactLoader(getActivity());
+ }
+ }
+
+ @Override
+ protected void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (loader.getId() == DISPLAY_NAME_LOADER) {
+ if (data != null && data.moveToFirst()) {
+ showTargetContactName(data.getString(0));
+ }
+ } else {
+ JoinContactListAdapter adapter = getAdapter();
+ Cursor suggestionsCursor = ((JoinContactLoader)loader).getSuggestionsCursor();
+ adapter.setSuggestionsCursor(suggestionsCursor);
+ super.onLoadFinished(loader, data);
+ }
+ }
+
+ private void showTargetContactName(String displayName) {
+ Activity activity = getActivity();
+ TextView blurbView = (TextView)activity.findViewById(R.id.join_contact_blurb);
+ String blurb = activity.getString(R.string.blurbJoinContactDataWith, displayName);
+ blurbView.setText(blurb);
+ }
+
+ public void setTargetContactId(long targetContactId) {
+ mTargetContactId = targetContactId;
+ }
+
+ @Override
+ public JoinContactListAdapter createListAdapter() {
+ return new JoinContactListAdapter(getActivity());
+ }
+
+ @Override
+ protected void configureAdapter() {
+ super.configureAdapter();
+ JoinContactListAdapter adapter = getAdapter();
+ adapter.setAllContactsListShown(mAllContactsListShown);
+ adapter.setTargetContactId(mTargetContactId);
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ return inflater.inflate(R.layout.contacts_list_content_join, null);
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ JoinContactListAdapter adapter = getAdapter();
+ int partition = adapter.getPartitionForPosition(position);
+ if (partition == JoinContactListAdapter.PARTITION_SHOW_ALL_CONTACTS) {
+ mAllContactsListShown = true;
+ reloadData();
+ } else {
+ mListener.onPickContactAction(adapter.getContactUri(position));
+ }
+ }
+}
diff --git a/src/com/android/contacts/list/JoinContactLoader.java b/src/com/android/contacts/list/JoinContactLoader.java
new file mode 100644
index 0000000..25c9ab4
--- /dev/null
+++ b/src/com/android/contacts/list/JoinContactLoader.java
@@ -0,0 +1,92 @@
+/*
+ * 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.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+
+/**
+ * A specialized loader for the Join Contacts UI. It executes two queries:
+ * join suggestions and (optionally) the full contact list.
+ */
+public class JoinContactLoader extends CursorLoader {
+
+ private boolean mLoadSuggestionsAndAllContact;
+ private String[] mProjection;
+ private Uri mSuggestionUri;
+ private MatrixCursor mSuggestionsCursor;
+
+ public JoinContactLoader(Context context) {
+ super(context, null, null, null, null, null);
+ }
+
+ public void setLoadSuggestionsAndAllContacts(boolean flag) {
+ mLoadSuggestionsAndAllContact = flag;
+ }
+
+ public void setSuggestionUri(Uri uri) {
+ this.mSuggestionUri = uri;
+ }
+
+ @Override
+ public void setProjection(String[] projection) {
+ super.setProjection(projection);
+ this.mProjection = projection;
+ }
+
+ public Cursor getSuggestionsCursor() {
+ return mSuggestionsCursor;
+ }
+
+ @Override
+ public Cursor loadInBackground() {
+ // First execute the suggestions query, then call super.loadInBackground
+ // to load the entire list
+ mSuggestionsCursor = loadSuggestions();
+ if (!mLoadSuggestionsAndAllContact && mSuggestionsCursor.getCount() != 0) {
+ // In case we only need suggestions, send "0" as the search query, which
+ // will always return an empty cursor (but we can still register to
+ // listen for changes on it).
+ setSelection("0");
+ setSelectionArgs(null);
+ }
+ return super.loadInBackground();
+ }
+
+ /**
+ * Loads join suggestions into a MatrixCursor.
+ */
+ private MatrixCursor loadSuggestions() {
+ Cursor cursor = getContext().getContentResolver().query(mSuggestionUri, mProjection,
+ null, null, null);
+ try {
+ MatrixCursor matrix = new MatrixCursor(mProjection);
+ Object[] row = new Object[mProjection.length];
+ while (cursor.moveToNext()) {
+ for (int i = 0; i < row.length; i++) {
+ row[i] = cursor.getString(i);
+ }
+ matrix.addRow(row);
+ }
+ return matrix;
+ } finally {
+ cursor.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/list/LegacyContactListAdapter.java b/src/com/android/contacts/list/LegacyContactListAdapter.java
new file mode 100644
index 0000000..39c0b53
--- /dev/null
+++ b/src/com/android/contacts/list/LegacyContactListAdapter.java
@@ -0,0 +1,95 @@
+/*
+ * 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.list;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Contacts.People;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A cursor adapter for the People.CONTENT_TYPE content type.
+ */
+@SuppressWarnings("deprecation")
+public class LegacyContactListAdapter extends ContactEntryListAdapter {
+
+ static final String[] PEOPLE_PROJECTION = new String[] {
+ People._ID, // 0
+ People.DISPLAY_NAME, // 1
+ People.PHONETIC_NAME, // 2
+ People.STARRED, // 3
+ People.PRESENCE_STATUS, // 4
+ };
+
+ protected static final int PERSON_ID_COLUMN_INDEX = 0;
+ protected static final int PERSON_DISPLAY_NAME_COLUMN_INDEX = 1;
+ protected static final int PERSON_PHONETIC_NAME_COLUMN_INDEX = 2;
+ protected static final int PERSON_STARRED_COLUMN_INDEX = 3;
+ protected static final int PERSON_PRESENCE_STATUS_COLUMN_INDEX = 4;
+
+ private CharSequence mUnknownNameText;
+
+ public LegacyContactListAdapter(Context context) {
+ super(context);
+ mUnknownNameText = context.getText(android.R.string.unknownName);
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ loader.setUri(People.CONTENT_URI);
+ loader.setProjection(PEOPLE_PROJECTION);
+ loader.setSortOrder(People.DISPLAY_NAME);
+ }
+
+ @Override
+ public String getContactDisplayName(int position) {
+ return ((Cursor)getItem(position)).getString(PERSON_DISPLAY_NAME_COLUMN_INDEX);
+ }
+
+ public Uri getPersonUri(int position) {
+ Cursor cursor = ((Cursor)getItem(position));
+ long personId = cursor.getLong(PERSON_ID_COLUMN_INDEX);
+ return ContentUris.withAppendedId(People.CONTENT_URI, personId);
+ }
+
+ @Override
+ protected View newView(Context context, int partition, Cursor cursor, int position,
+ ViewGroup parent) {
+ final ContactListItemView view = new ContactListItemView(context, null);
+ view.setUnknownNameText(mUnknownNameText);
+ return view;
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ ContactListItemView view = (ContactListItemView)itemView;
+ bindName(view, cursor);
+ bindPresence(view, cursor);
+ }
+
+ protected void bindName(final ContactListItemView view, Cursor cursor) {
+ view.showDisplayName(cursor, PERSON_DISPLAY_NAME_COLUMN_INDEX, false, 0);
+ view.showPhoneticName(cursor, PERSON_PHONETIC_NAME_COLUMN_INDEX);
+ }
+
+ protected void bindPresence(final ContactListItemView view, Cursor cursor) {
+ view.showPresence(cursor, PERSON_PRESENCE_STATUS_COLUMN_INDEX);
+ }
+}
diff --git a/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java b/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java
new file mode 100644
index 0000000..47747fb
--- /dev/null
+++ b/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java
@@ -0,0 +1,108 @@
+/*
+ * 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.list;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Contacts.People;
+import android.provider.Contacts.Phones;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A cursor adapter for the Phones.CONTENT_TYPE content type.
+ */
+@SuppressWarnings("deprecation")
+public class LegacyPhoneNumberListAdapter extends ContactEntryListAdapter {
+
+ private static final String[] PHONES_PROJECTION = new String[] {
+ Phones._ID, // 0
+ Phones.TYPE, // 1
+ Phones.LABEL, // 2
+ Phones.NUMBER, // 3
+ People.DISPLAY_NAME, // 4
+ People.PHONETIC_NAME, // 5
+ };
+
+ private static final int PHONE_ID_COLUMN_INDEX = 0;
+ private static final int PHONE_TYPE_COLUMN_INDEX = 1;
+ private static final int PHONE_LABEL_COLUMN_INDEX = 2;
+ private static final int PHONE_NUMBER_COLUMN_INDEX = 3;
+ private static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 4;
+ private static final int PHONE_PHONETIC_NAME_COLUMN_INDEX = 5;
+
+ private CharSequence mUnknownNameText;
+
+ public LegacyPhoneNumberListAdapter(Context context) {
+ super(context);
+ mUnknownNameText = context.getText(android.R.string.unknownName);
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ loader.setUri(Phones.CONTENT_URI);
+ loader.setProjection(PHONES_PROJECTION);
+ loader.setSortOrder(Phones.DISPLAY_NAME);
+ }
+
+ @Override
+ public String getContactDisplayName(int position) {
+ return ((Cursor)getItem(position)).getString(PHONE_DISPLAY_NAME_COLUMN_INDEX);
+ }
+
+ public Uri getPhoneUri(int position) {
+ Cursor cursor = ((Cursor)getItem(position));
+ long id = cursor.getLong(PHONE_ID_COLUMN_INDEX);
+ return ContentUris.withAppendedId(Phones.CONTENT_URI, id);
+ }
+
+ @Override
+ protected View newView(Context context, int partition, Cursor cursor, int position,
+ ViewGroup parent) {
+ final ContactListItemView view = new ContactListItemView(context, null);
+ view.setUnknownNameText(mUnknownNameText);
+ return view;
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ ContactListItemView view = (ContactListItemView)itemView;
+ bindName(view, cursor);
+ bindPhoneNumber(view, cursor);
+ }
+
+ protected void bindName(final ContactListItemView view, Cursor cursor) {
+ view.showDisplayName(cursor, PHONE_DISPLAY_NAME_COLUMN_INDEX, false, 0);
+ view.showPhoneticName(cursor, PHONE_PHONETIC_NAME_COLUMN_INDEX);
+ }
+
+ protected void bindPhoneNumber(ContactListItemView view, Cursor cursor) {
+ CharSequence label = null;
+ if (!cursor.isNull(PHONE_TYPE_COLUMN_INDEX)) {
+ final int type = cursor.getInt(PHONE_TYPE_COLUMN_INDEX);
+ final String customLabel = cursor.getString(PHONE_LABEL_COLUMN_INDEX);
+
+ // TODO cache
+ label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
+ }
+ view.setLabel(label);
+ view.showData(cursor, PHONE_NUMBER_COLUMN_INDEX);
+ }
+}
diff --git a/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java b/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java
new file mode 100644
index 0000000..3796c62
--- /dev/null
+++ b/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java
@@ -0,0 +1,109 @@
+/*
+ * 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.list;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.People;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A cursor adapter for the ContactMethods.CONTENT_TYPE content type.
+ */
+@SuppressWarnings("deprecation")
+public class LegacyPostalAddressListAdapter extends ContactEntryListAdapter {
+
+ static final String[] POSTALS_PROJECTION = new String[] {
+ ContactMethods._ID, // 0
+ ContactMethods.TYPE, // 1
+ ContactMethods.LABEL, // 2
+ ContactMethods.DATA, // 3
+ People.DISPLAY_NAME, // 4
+ People.PHONETIC_NAME, // 5
+ };
+
+ public static final int POSTAL_ID_COLUMN_INDEX = 0;
+ public static final int POSTAL_TYPE_COLUMN_INDEX = 1;
+ public static final int POSTAL_LABEL_COLUMN_INDEX = 2;
+ public static final int POSTAL_NUMBER_COLUMN_INDEX = 3;
+ public static final int POSTAL_DISPLAY_NAME_COLUMN_INDEX = 4;
+ public static final int POSTAL_PHONETIC_NAME_COLUMN_INDEX = 5;
+
+ private CharSequence mUnknownNameText;
+
+ public LegacyPostalAddressListAdapter(Context context) {
+ super(context);
+ mUnknownNameText = context.getText(android.R.string.unknownName);
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ loader.setUri(ContactMethods.CONTENT_URI);
+ loader.setProjection(POSTALS_PROJECTION);
+ loader.setSortOrder(People.DISPLAY_NAME);
+ loader.setSelection(ContactMethods.KIND + "=" + android.provider.Contacts.KIND_POSTAL);
+ }
+
+ @Override
+ public String getContactDisplayName(int position) {
+ return ((Cursor)getItem(position)).getString(POSTAL_DISPLAY_NAME_COLUMN_INDEX);
+ }
+
+ public Uri getContactMethodUri(int position) {
+ Cursor cursor = ((Cursor)getItem(position));
+ long id = cursor.getLong(POSTAL_ID_COLUMN_INDEX);
+ return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id);
+ }
+
+ @Override
+ protected View newView(Context context, int partition, Cursor cursor, int position,
+ ViewGroup parent) {
+ final ContactListItemView view = new ContactListItemView(context, null);
+ view.setUnknownNameText(mUnknownNameText);
+ return view;
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ ContactListItemView view = (ContactListItemView)itemView;
+ bindName(view, cursor);
+ bindPostalAddress(view, cursor);
+ }
+
+ protected void bindName(final ContactListItemView view, Cursor cursor) {
+ view.showDisplayName(cursor, POSTAL_DISPLAY_NAME_COLUMN_INDEX, false, 0);
+ view.showPhoneticName(cursor, POSTAL_PHONETIC_NAME_COLUMN_INDEX);
+ }
+
+ protected void bindPostalAddress(ContactListItemView view, Cursor cursor) {
+ CharSequence label = null;
+ if (!cursor.isNull(POSTAL_TYPE_COLUMN_INDEX)) {
+ final int type = cursor.getInt(POSTAL_TYPE_COLUMN_INDEX);
+ final String customLabel = cursor.getString(POSTAL_LABEL_COLUMN_INDEX);
+
+ // TODO cache
+ label = StructuredPostal.getTypeLabel(getContext().getResources(), type, customLabel);
+ }
+ view.setLabel(label);
+ view.showData(cursor, POSTAL_NUMBER_COLUMN_INDEX);
+ }
+}
diff --git a/src/com/android/contacts/list/MultiplePhonePickerAdapter.java b/src/com/android/contacts/list/MultiplePhonePickerAdapter.java
new file mode 100644
index 0000000..2b1196b
--- /dev/null
+++ b/src/com/android/contacts/list/MultiplePhonePickerAdapter.java
@@ -0,0 +1,361 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.text.TextUtils;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+
+/**
+ * List adapter for the multiple phone picker.
+ */
+public class MultiplePhonePickerAdapter extends PhoneNumberListAdapter {
+
+ public interface OnSelectionChangeListener {
+ void onSelectionChange();
+ }
+
+ private static final 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,
+ };
+
+ public static final long INVALID_PHONE_ID = -1;
+
+ /** The phone numbers */
+ private ArrayList<String> mPhoneNumbers = new ArrayList<String>();
+
+ /** The selected phone numbers in the PhoneNumberAdapter */
+ private HashSet<String> mSelectedPhoneNumbers = new HashSet<String>();
+
+ /** The phone numbers after the filtering */
+ private ArrayList<String> mFilteredPhoneNumbers = new ArrayList<String>();
+
+ /** The PHONE_ID of selected number in user contacts*/
+ private HashSet<Long> mSelectedPhoneIds = new HashSet<Long>();
+
+ private boolean mSelectionChanged;
+
+ private OnSelectionChangeListener mSelectionChangeListener;
+
+ /**
+ * This is a map from contact ID to color index. A colored chip is used to
+ * indicate the number of phone numbers belong to one contact
+ */
+ private SparseIntArray mContactColor = new SparseIntArray();
+
+ public MultiplePhonePickerAdapter(Context context) {
+ super(context);
+ }
+
+ public void setOnSelectionChangeListener(OnSelectionChangeListener listener) {
+ this.mSelectionChangeListener = listener;
+ }
+
+ public void setPhoneNumbers(ArrayList<String> phoneNumbers) {
+ mPhoneNumbers.clear();
+ mPhoneNumbers.addAll(phoneNumbers);
+ }
+
+ public int getSelectedCount() {
+ return mSelectedPhoneNumbers.size() + mSelectedPhoneIds.size();
+ }
+
+ public Uri[] getSelectedUris() {
+ Uri[] uris = new Uri[mSelectedPhoneNumbers.size() + mSelectedPhoneIds.size()];
+ int count = mPhoneNumbers.size();
+ int index = 0;
+ for (int i = 0; i < count; i++) {
+ String phoneNumber = mPhoneNumbers.get(i);
+ if (isSelected(phoneNumber)) {
+ uris[index++] = Uri.parse("tel:" + phoneNumber);
+ }
+ }
+ for (Long contactId : mSelectedPhoneIds) {
+ uris[index++] = ContentUris.withAppendedId(Phone.CONTENT_URI, contactId);
+ }
+ return uris;
+ }
+
+ public void setSelectedUris(Uri[] uris) {
+ mSelectedPhoneNumbers.clear();
+ mSelectedPhoneIds.clear();
+ if (uris != null) {
+ for (Uri uri : uris) {
+ String scheme = uri.getScheme();
+ if ("tel".equals(scheme)) {
+ String phoneNumber = uri.getSchemeSpecificPart();
+ if (!mPhoneNumbers.contains(phoneNumber)) {
+ mPhoneNumbers.add(phoneNumber);
+ }
+ mSelectedPhoneNumbers.add(phoneNumber);
+ } else if ("content".equals(scheme)) {
+ mSelectedPhoneIds.add(ContentUris.parseId(uri));
+ }
+ }
+ }
+ mFilteredPhoneNumbers.clear();
+ mFilteredPhoneNumbers.addAll(mPhoneNumbers);
+ }
+
+ public void toggleSelection(int position) {
+ if (position < mFilteredPhoneNumbers.size()) {
+ String phoneNumber = mPhoneNumbers.get(position);
+ setPhoneSelected(phoneNumber, !isSelected(phoneNumber));
+ } else {
+ Cursor cursor = ((Cursor)getItem(position));
+ cursor.moveToPosition(position - mFilteredPhoneNumbers.size());
+ long phoneId = cursor.getLong(PHONE_ID_COLUMN_INDEX);
+ setPhoneSelected(phoneId, !isSelected(phoneId));
+ }
+ notifyDataSetChanged();
+ }
+
+ public boolean isSelectionChanged() {
+ return mSelectionChanged;
+ }
+
+ public void setSelectionChanged(boolean flag) {
+ mSelectionChanged = flag;
+ if (mSelectionChangeListener != null) {
+ mSelectionChangeListener.onSelectionChange();
+ }
+ }
+
+ public void setPhoneSelected(final String phoneNumber, boolean selected) {
+ if (!TextUtils.isEmpty(phoneNumber)) {
+ if (selected) {
+ mSelectedPhoneNumbers.add(phoneNumber);
+ } else {
+ mSelectedPhoneNumbers.remove(phoneNumber);
+ }
+ }
+ setSelectionChanged(true);
+ }
+
+ public void setPhoneSelected(long phoneId, boolean selected) {
+ if (selected) {
+ mSelectedPhoneIds.add(phoneId);
+ } else {
+ mSelectedPhoneIds.remove(phoneId);
+ }
+ setSelectionChanged(true);
+ }
+
+ 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 = this.mMultiplePhoneSelectionActivity.mAdapter.getCursor();
+// if (cursor != null) {
+// int backupPos = cursor.getPosition();
+// cursor.moveToPosition(-1);
+// while (cursor.moveToNext()) {
+// setPhoneSelected(cursor
+// .getLong(MultiplePhonePickerActivity.PHONE_ID_COLUMN_INDEX), true);
+// }
+// cursor.moveToPosition(backupPos);
+// }
+// for (String number : this.mMultiplePhoneSelectionActivity.mPhoneNumberAdapter
+// .getFilteredPhoneNumbers()) {
+// setPhoneSelected(number, true);
+// }
+// } else {
+// mSelectedPhoneIds.clear();
+// mSelectedPhoneNumbers.clear();
+// }
+ }
+
+ public boolean isAllSelected() {
+ return false;
+// return selectedCount() == this.mMultiplePhoneSelectionActivity.mPhoneNumberAdapter
+// .getFilteredPhoneNumbers().size()
+// + this.mMultiplePhoneSelectionActivity.mAdapter.getCount();
+ }
+
+ public Iterator<Long> getSelectedPhoneIds() {
+ return mSelectedPhoneIds.iterator();
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return position < mPhoneNumbers.size() ? 0 : 1;
+ }
+
+ // TODO redo as two separate partitions
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = null;
+// if (convertView == null || convertView.getTag() == null) {
+// view = newView(getContext(), null, parent);
+// } else {
+// view = convertView;
+// }
+//
+// boolean showingSuggestion = false;
+//
+// if (position < mFilteredPhoneNumbers.size()) {
+// bindExtraPhoneView(view, position);
+// } else {
+// Cursor cursor = ((Cursor)getItem(position));
+// cursor.moveToPosition(position - mFilteredPhoneNumbers.size());
+// bindView(view, getContext(), cursor);
+// }
+ return view;
+ }
+
+ @Override
+ protected View newView(Context context, int partition, Cursor cursor, int position,
+ ViewGroup parent) {
+ final MultiplePhonePickerItemView view = new MultiplePhonePickerItemView(context, null);
+ view.setUnknownNameText(getUnknownNameText());
+ view.setTextWithHighlightingFactory(getTextWithHighlightingFactory());
+ return view;
+ }
+
+ private void bindExtraPhoneView(View itemView, int position) {
+ final MultiplePhonePickerItemView view = (MultiplePhonePickerItemView)itemView;
+ String phoneNumber = mFilteredPhoneNumbers.get(position);
+ view.getNameTextView().setText(phoneNumber);
+ CheckBox checkBox = view.getCheckBoxView();
+ checkBox.setChecked(isSelected(phoneNumber));
+ view.phoneId = INVALID_PHONE_ID;
+ view.phoneNumber = phoneNumber;
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ super.bindView(itemView, partition, cursor, position);
+
+ final MultiplePhonePickerItemView view = (MultiplePhonePickerItemView)itemView;
+ view.phoneId = Long.valueOf(cursor.getLong(PHONE_ID_COLUMN_INDEX));
+ CheckBox checkBox = view.getCheckBoxView();
+ checkBox.setChecked(isSelected(view.phoneId));
+
+ long contactId = cursor.getLong(PHONE_CONTACT_ID_COLUMN_INDEX);
+ view.getChipView().setBackgroundResource(getChipColor(contactId));
+ }
+
+// @Override
+// protected void prepareEmptyView() {
+// mMultiplePhonePickerActivity.mEmptyView.show(mMultiplePhonePickerActivity.mSearchMode,
+// true, false, false, false, true, mMultiplePhonePickerActivity.mShowSelectedOnly);
+// }
+
+ /**
+ * Get assigned chip color resource id for a given contact, 0 is returned if there is no mapped
+ * resource.
+ */
+ public int getChipColor(long contactId) {
+ return mContactColor.get((int)contactId);
+ }
+
+ // TODO filtering
+// public void doFilter(final String constraint, boolean selectedOnly) {
+// if (mPhoneNumbers == null) {
+// return;
+// }
+// mFilteredPhoneNumbers.clear();
+// for (String number : mPhoneNumbers) {
+// if (selectedOnly && !mSelection.isSelected(number) ||
+// !TextUtils.isEmpty(constraint) && !number.startsWith(constraint)) {
+// continue;
+// }
+// mFilteredPhoneNumbers.add(number);
+// }
+// }
+
+ @Override
+ public int getCount() {
+ return super.getCount() + mFilteredPhoneNumbers.size();
+ }
+
+ @Override
+ public void changeCursor(Cursor cursor) {
+ super.changeCursor(cursor);
+ updateChipColor(cursor);
+ }
+
+ /**
+ * Go through the cursor and assign the chip color to contact who has more
+ * than one phone numbers. Assume the cursor is clustered by CONTACT_ID.
+ */
+ public void updateChipColor(Cursor cursor) {
+ if (cursor == null || cursor.getCount() == 0) {
+ return;
+ }
+ mContactColor.clear();
+ cursor.moveToFirst();
+ int colorIndex = 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((int)contactId) < 0) {
+ mContactColor.put((int)contactId, CHIP_COLOR_ARRAY[colorIndex]);
+ colorIndex++;
+ if (colorIndex >= CHIP_COLOR_ARRAY.length) {
+ colorIndex = 0;
+ }
+ }
+ }
+ prevContactId = contactId;
+ }
+ }
+}
diff --git a/src/com/android/contacts/list/MultiplePhonePickerFragment.java b/src/com/android/contacts/list/MultiplePhonePickerFragment.java
new file mode 100644
index 0000000..bf75a9f
--- /dev/null
+++ b/src/com/android/contacts/list/MultiplePhonePickerFragment.java
@@ -0,0 +1,181 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+import com.android.contacts.list.MultiplePhonePickerAdapter.OnSelectionChangeListener;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.View.OnClickListener;
+import android.view.animation.AnimationUtils;
+import android.widget.Button;
+
+/**
+ * Fragment for the multiple phone picker.
+ */
+public class MultiplePhonePickerFragment
+ extends ContactEntryListFragment<MultiplePhonePickerAdapter>
+ implements OnClickListener, OnSelectionChangeListener {
+
+ private static final String SELECTION_EXTRA_KEY = "selection";
+ private static final String SELECTION_CHANGED_EXTRA_KEY = "selectionChanged";
+
+ private OnMultiplePhoneNumberPickerActionListener mListener;
+
+ /**
+ * UI control of action panel in MODE_PICK_MULTIPLE_PHONES mode.
+ */
+ private View mFooterView;
+
+ private Uri[] mSelectedUris;
+ private boolean mSelectionChanged;
+
+ public MultiplePhonePickerFragment() {
+ setSectionHeaderDisplayEnabled(false);
+ setPhotoLoaderEnabled(true);
+ }
+
+ public void setOnMultiplePhoneNumberPickerActionListener(
+ OnMultiplePhoneNumberPickerActionListener listener) {
+ mListener = listener;
+ }
+
+ public Uri[] getSelectedUris() {
+ return getAdapter().getSelectedUris();
+ }
+
+ public void setSelectedUris(Parcelable[] extras) {
+ Uri[] uris = new Uri[extras == null ? 0 : extras.length];
+ if (extras != null) {
+ for (int i = 0; i < extras.length; i++) {
+ uris[i] = (Uri)extras[i];
+ }
+ }
+ setSelectedUris(uris);
+ }
+
+ public void setSelectedUris(Uri[] uris) {
+ mSelectedUris = uris;
+ MultiplePhonePickerAdapter adapter = getAdapter();
+ if (adapter != null) {
+ adapter.setSelectedUris(uris);
+ }
+ }
+
+ @Override
+ protected MultiplePhonePickerAdapter createListAdapter() {
+ return new MultiplePhonePickerAdapter(getActivity());
+ }
+
+ @Override
+ protected void configureAdapter() {
+ super.configureAdapter();
+ MultiplePhonePickerAdapter adapter = getAdapter();
+ adapter.setSelectedUris(mSelectedUris);
+ adapter.setSelectionChanged(mSelectionChanged);
+ adapter.setOnSelectionChangeListener(this);
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ View view = inflater.inflate(R.layout.contacts_list_content, null);
+ ViewStub stub = (ViewStub)view.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);
+ }
+ return view;
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ getAdapter().toggleSelection(position);
+ }
+
+ public void onClick(View v) {
+ int id = v.getId();
+ switch (id) {
+ case R.id.done:
+ mListener.onPhoneNumbersSelectedAction(getAdapter().getSelectedUris());
+ break;
+ case R.id.revert:
+ mListener.onFinishAction();
+ break;
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+ if (savedState != null) {
+ setSelectedUris(savedState.getParcelableArray(SELECTION_EXTRA_KEY));
+ mSelectionChanged = savedState.getBoolean(SELECTION_CHANGED_EXTRA_KEY, false);
+ if (getAdapter() != null) {
+ getAdapter().setSelectionChanged(mSelectionChanged);
+ }
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ updateWidgets();
+ }
+
+ public void onSelectionChange() {
+ updateWidgets();
+ }
+
+ private void updateWidgets() {
+ int selected = getAdapter().getSelectedCount();
+
+ Activity context = getActivity();
+ if (selected >= 1) {
+ final String format = context.getResources().getQuantityString(
+ R.plurals.multiple_picker_title, selected);
+
+ // TODO: turn this into a callback
+ context.setTitle(String.format(format, selected));
+ } else {
+ // TODO: turn this into a callback
+ context.setTitle(context.getString(R.string.contactsList));
+ }
+
+ if (getAdapter().isSelectionChanged() && mFooterView.getVisibility() == View.GONE) {
+ mFooterView.setVisibility(View.VISIBLE);
+ mFooterView.startAnimation(AnimationUtils.loadAnimation(context, R.anim.footer_appear));
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle icicle) {
+ super.onSaveInstanceState(icicle);
+ icicle.putParcelableArray(SELECTION_EXTRA_KEY, getAdapter().getSelectedUris());
+ icicle.putBoolean(SELECTION_CHANGED_EXTRA_KEY, getAdapter().isSelectionChanged());
+ }
+}
diff --git a/src/com/android/contacts/list/MultiplePhonePickerItemView.java b/src/com/android/contacts/list/MultiplePhonePickerItemView.java
new file mode 100644
index 0000000..4801d33
--- /dev/null
+++ b/src/com/android/contacts/list/MultiplePhonePickerItemView.java
@@ -0,0 +1,121 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.CheckBox;
+
+/**
+ * A custom view for an item in the phone multi-picker list.
+ */
+public class MultiplePhonePickerItemView extends ContactListItemView {
+
+ // 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 mChipWidth;
+ private int mChipRightMargin;
+ private int mCheckBoxMargin;
+
+ public long phoneId;
+ // phoneNumber only validates when phoneId = INVALID_PHONE_ID
+ public String phoneNumber;
+
+ public MultiplePhonePickerItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ Resources resources = context.getResources();
+ 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);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (isVisible(mChipView)) {
+ mChipView.measure(0, 0);
+ }
+
+ if (isVisible(mCheckBox)) {
+ mCheckBox.measure(0, 0);
+ }
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected int layoutLeftSide(int height, int topBound, int leftBound) {
+ if (mChipView != null) {
+ mChipView.layout(leftBound, topBound, leftBound + mChipWidth, height);
+ leftBound += mChipWidth + mChipRightMargin;
+ }
+
+ return super.layoutLeftSide(height, topBound, leftBound);
+ }
+
+ @Override
+ protected int layoutRightSide(int height, int topBound, int rightBound) {
+ rightBound = super.layoutRightSide(height, topBound, rightBound);
+
+ 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);
+ }
+
+ return rightBound;
+ }
+
+ /**
+ * 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.setClickable(false);
+ mCheckBox.setFocusable(false);
+ addView(mCheckBox);
+ }
+ return mCheckBox;
+ }
+}
diff --git a/src/com/android/contacts/list/OnContactBrowserActionListener.java b/src/com/android/contacts/list/OnContactBrowserActionListener.java
new file mode 100644
index 0000000..56f9bbc
--- /dev/null
+++ b/src/com/android/contacts/list/OnContactBrowserActionListener.java
@@ -0,0 +1,74 @@
+/*
+ * 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.list;
+
+import android.net.Uri;
+
+/**
+ * Action callbacks that can be sent by a contact list.
+ */
+public interface OnContactBrowserActionListener {
+
+ /**
+ * Searches all contacts for the specified string an show results for browsing.
+ */
+ void onSearchAllContactsAction(String string);
+
+ /**
+ * Opens the specified contact for viewing.
+ */
+ void onViewContactAction(Uri contactLookupUri);
+
+ /**
+ * Creates a new contact.
+ */
+ void onCreateNewContactAction();
+
+ /**
+ * Opens the specified contact for editing.
+ */
+ void onEditContactAction(Uri contactLookupUri);
+
+ /**
+ * Initiates the contact deletion process.
+ */
+ void onDeleteContactAction(Uri contactUri);
+
+ /**
+ * Adds the specified contact to favorites
+ */
+ void onAddToFavoritesAction(Uri contactUri);
+
+ /**
+ * Removes the specified contact from favorites.
+ */
+ void onRemoveFromFavoritesAction(Uri contactUri);
+
+ /**
+ * Places a call to the specified contact.
+ */
+ void onCallContactAction(Uri contactUri);
+
+ /**
+ * Initiates a text message to the specified contact.
+ */
+ void onSmsContactAction(Uri contactUri);
+
+ /**
+ * Closes the contact browser.
+ */
+ void onFinishAction();
+}
diff --git a/src/com/android/contacts/list/OnContactPickerActionListener.java b/src/com/android/contacts/list/OnContactPickerActionListener.java
new file mode 100644
index 0000000..7245fbc
--- /dev/null
+++ b/src/com/android/contacts/list/OnContactPickerActionListener.java
@@ -0,0 +1,45 @@
+/*
+ * 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.list;
+
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ * Action callbacks that can be sent by a contact picker.
+ */
+public interface OnContactPickerActionListener {
+
+ /**
+ * Searches all contacts for the specified string an show results for browsing.
+ */
+ void onSearchAllContactsAction(String string);
+
+ /**
+ * Creates a new contact and then returns it to the caller.
+ */
+ void onCreateNewContactAction();
+
+ /**
+ * Returns the selected contact to the requester.
+ */
+ void onPickContactAction(Uri contactUri);
+
+ /**
+ * Returns the selected contact as a shortcut intent.
+ */
+ void onShortcutIntentCreated(Intent intent);
+}
diff --git a/src/com/android/contacts/list/OnMultiplePhoneNumberPickerActionListener.java b/src/com/android/contacts/list/OnMultiplePhoneNumberPickerActionListener.java
new file mode 100644
index 0000000..ac010ba
--- /dev/null
+++ b/src/com/android/contacts/list/OnMultiplePhoneNumberPickerActionListener.java
@@ -0,0 +1,34 @@
+/*
+ * 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.list;
+
+import android.net.Uri;
+
+/**
+ * Action callbacks that can be sent by a multiple phone number picker.
+ */
+public interface OnMultiplePhoneNumberPickerActionListener {
+
+ /**
+ * Returns the selected phone numbers to the requester.
+ */
+ void onPhoneNumbersSelectedAction(Uri[] dataUris);
+
+ /**
+ * Closes the picker without changing the selection.
+ */
+ void onFinishAction();
+}
diff --git a/src/com/android/contacts/list/OnPhoneNumberPickerActionListener.java b/src/com/android/contacts/list/OnPhoneNumberPickerActionListener.java
new file mode 100644
index 0000000..701cc78
--- /dev/null
+++ b/src/com/android/contacts/list/OnPhoneNumberPickerActionListener.java
@@ -0,0 +1,40 @@
+/*
+ * 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.list;
+
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ * Action callbacks that can be sent by a phone number picker.
+ */
+public interface OnPhoneNumberPickerActionListener {
+
+ /**
+ * Returns the selected phone number to the requester.
+ */
+ void onPickPhoneNumberAction(Uri dataUri);
+
+ /**
+ * Returns the selected number as a shortcut intent.
+ */
+ void onShortcutIntentCreated(Intent intent);
+
+ /**
+ * Searches all contacts for the specified string an show results for browsing.
+ */
+ void onSearchAllContactsAction(String string);
+}
diff --git a/src/com/android/contacts/list/OnPostalAddressPickerActionListener.java b/src/com/android/contacts/list/OnPostalAddressPickerActionListener.java
new file mode 100644
index 0000000..a43dfe4
--- /dev/null
+++ b/src/com/android/contacts/list/OnPostalAddressPickerActionListener.java
@@ -0,0 +1,34 @@
+/*
+ * 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.list;
+
+import android.net.Uri;
+
+/**
+ * Action callbacks that can be sent by a postal address picker.
+ */
+public interface OnPostalAddressPickerActionListener {
+
+ /**
+ * Returns the selected phone number to the requester.
+ */
+ void onPickPostalAddressAction(Uri dataUri);
+
+ /**
+ * Searches all contacts for the specified string an show results for browsing.
+ */
+ void onSearchAllContactsAction(String string);
+}
diff --git a/src/com/android/contacts/list/PhoneNumberListAdapter.java b/src/com/android/contacts/list/PhoneNumberListAdapter.java
new file mode 100644
index 0000000..0bfd6dc
--- /dev/null
+++ b/src/com/android/contacts/list/PhoneNumberListAdapter.java
@@ -0,0 +1,220 @@
+/*
+ * 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.list;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A cursor adapter for the {@link Phone#CONTENT_TYPE} content type.
+ */
+public class PhoneNumberListAdapter extends ContactEntryListAdapter {
+
+ protected static final String[] PHONES_PROJECTION = new String[] {
+ Phone._ID, // 0
+ Phone.TYPE, // 1
+ Phone.LABEL, // 2
+ Phone.NUMBER, // 3
+ Phone.DISPLAY_NAME_PRIMARY, // 4
+ Phone.DISPLAY_NAME_ALTERNATIVE, // 5
+ Phone.CONTACT_ID, // 6
+ Phone.PHOTO_ID, // 7
+ Phone.PHONETIC_NAME, // 8
+ };
+
+ protected static final int PHONE_ID_COLUMN_INDEX = 0;
+ protected static final int PHONE_TYPE_COLUMN_INDEX = 1;
+ protected static final int PHONE_LABEL_COLUMN_INDEX = 2;
+ protected static final int PHONE_NUMBER_COLUMN_INDEX = 3;
+ protected static final int PHONE_PRIMARY_DISPLAY_NAME_COLUMN_INDEX = 4;
+ protected static final int PHONE_ALTERNATIVE_DISPLAY_NAME_COLUMN_INDEX = 5;
+ protected static final int PHONE_CONTACT_ID_COLUMN_INDEX = 6;
+ protected static final int PHONE_PHOTO_ID_COLUMN_INDEX = 7;
+ protected static final int PHONE_PHONETIC_NAME_COLUMN_INDEX = 8;
+
+ private CharSequence mUnknownNameText;
+ private int mDisplayNameColumnIndex;
+ private int mAlternativeDisplayNameColumnIndex;
+ private boolean mVisibleContactsOnly = true;
+
+ public PhoneNumberListAdapter(Context context) {
+ super(context);
+
+ mUnknownNameText = context.getText(android.R.string.unknownName);
+ }
+
+ protected CharSequence getUnknownNameText() {
+ return mUnknownNameText;
+ }
+
+ public void setVisibleContactsOnly(boolean flag) {
+ mVisibleContactsOnly = flag;
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ Uri uri;
+
+ if (isSearchMode()) {
+ String query = getQueryString();
+ Builder builder = Phone.CONTENT_FILTER_URI.buildUpon();
+ if (TextUtils.isEmpty(query)) {
+ builder.appendPath("");
+ } else {
+ builder.appendPath(query); // Builder will encode the query
+ }
+
+ builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+ String.valueOf(directoryId));
+ uri = builder.build();
+ // TODO a projection that includes the search snippet
+ loader.setProjection(PHONES_PROJECTION);
+ } else {
+ uri = Phone.CONTENT_URI;
+ loader.setProjection(PHONES_PROJECTION);
+ }
+
+ if (directoryId == Directory.DEFAULT) {
+ if (mVisibleContactsOnly) {
+ loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1");
+ }
+ if (isSectionHeaderDisplayEnabled()) {
+ uri = buildSectionIndexerUri(uri);
+ }
+ }
+
+ loader.setUri(uri);
+ if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
+ loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
+ } else {
+ loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
+ }
+ }
+
+ protected static Uri buildSectionIndexerUri(Uri uri) {
+ return uri.buildUpon()
+ .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
+ }
+
+ @Override
+ public String getContactDisplayName(int position) {
+ return ((Cursor)getItem(position)).getString(mDisplayNameColumnIndex);
+ }
+
+ @Override
+ public void setContactNameDisplayOrder(int displayOrder) {
+ super.setContactNameDisplayOrder(displayOrder);
+ if (getContactNameDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+ mDisplayNameColumnIndex = PHONE_PRIMARY_DISPLAY_NAME_COLUMN_INDEX;
+ mAlternativeDisplayNameColumnIndex = PHONE_ALTERNATIVE_DISPLAY_NAME_COLUMN_INDEX;
+ } else {
+ mDisplayNameColumnIndex = PHONE_ALTERNATIVE_DISPLAY_NAME_COLUMN_INDEX;
+ mAlternativeDisplayNameColumnIndex = PHONE_PRIMARY_DISPLAY_NAME_COLUMN_INDEX;
+ }
+ }
+
+ /**
+ * Builds a {@link Data#CONTENT_URI} for the given cursor position.
+ */
+ public Uri getDataUri(int position) {
+ Cursor cursor = ((Cursor)getItem(position));
+ long id = cursor.getLong(PHONE_ID_COLUMN_INDEX);
+ return ContentUris.withAppendedId(Data.CONTENT_URI, id);
+ }
+
+ @Override
+ protected View newView(Context context, int partition, Cursor cursor, int position,
+ ViewGroup parent) {
+ final ContactListItemView view = new ContactListItemView(context, null);
+ view.setUnknownNameText(mUnknownNameText);
+ view.setTextWithHighlightingFactory(getTextWithHighlightingFactory());
+ return view;
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ ContactListItemView view = (ContactListItemView)itemView;
+ bindSectionHeaderAndDivider(view, position);
+ bindName(view, cursor);
+ bindPhoto(view, cursor);
+ bindPhoneNumber(view, cursor);
+ }
+
+ protected void bindPhoneNumber(ContactListItemView view, Cursor cursor) {
+ CharSequence label = null;
+ if (!cursor.isNull(PHONE_TYPE_COLUMN_INDEX)) {
+ final int type = cursor.getInt(PHONE_TYPE_COLUMN_INDEX);
+ final String customLabel = cursor.getString(PHONE_LABEL_COLUMN_INDEX);
+
+ // TODO cache
+ label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
+ }
+ view.setLabel(label);
+ view.showData(cursor, PHONE_NUMBER_COLUMN_INDEX);
+ }
+
+ protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
+ final int section = getSectionForPosition(position);
+ if (getPositionForSection(section) == position) {
+ String title = (String)getSections()[section];
+ view.setSectionHeader(title);
+ } else {
+ view.setDividerVisible(false);
+ view.setSectionHeader(null);
+ }
+
+ // move the divider for the last item in a section
+ if (getPositionForSection(section + 1) - 1 == position) {
+ view.setDividerVisible(false);
+ } else {
+ view.setDividerVisible(true);
+ }
+ }
+
+ protected void bindName(final ContactListItemView view, Cursor cursor) {
+ view.showDisplayName(cursor, mDisplayNameColumnIndex, isNameHighlightingEnabled(),
+ mAlternativeDisplayNameColumnIndex);
+ view.showPhoneticName(cursor, PHONE_PHONETIC_NAME_COLUMN_INDEX);
+ }
+
+ protected void bindPhoto(final ContactListItemView view, Cursor cursor) {
+ long photoId = 0;
+ if (!cursor.isNull(PHONE_PHOTO_ID_COLUMN_INDEX)) {
+ photoId = cursor.getLong(PHONE_PHOTO_ID_COLUMN_INDEX);
+ }
+
+ getPhotoLoader().loadPhoto(view.getPhotoView(), photoId);
+ }
+//
+// protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
+// view.showSnippet(cursor, SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX,
+// SUMMARY_SNIPPET_DATA1_COLUMN_INDEX, SUMMARY_SNIPPET_DATA4_COLUMN_INDEX);
+// }
+
+}
diff --git a/src/com/android/contacts/list/PhoneNumberPickerFragment.java b/src/com/android/contacts/list/PhoneNumberPickerFragment.java
new file mode 100644
index 0000000..cf787d2
--- /dev/null
+++ b/src/com/android/contacts/list/PhoneNumberPickerFragment.java
@@ -0,0 +1,109 @@
+/*
+ * 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.list;
+
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+import com.android.contacts.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Fragment containing a phone number list for picking.
+ */
+public class PhoneNumberPickerFragment extends ContactEntryListFragment<ContactEntryListAdapter>
+ implements OnShortcutIntentCreatedListener {
+ private OnPhoneNumberPickerActionListener mListener;
+ private String mShortcutAction;
+
+ public PhoneNumberPickerFragment() {
+ setPhotoLoaderEnabled(true);
+ }
+
+ public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) {
+ this.mListener = listener;
+ }
+
+ /**
+ * @param shortcutAction either {@link Intent#ACTION_CALL} or
+ * {@link Intent#ACTION_SENDTO} or null.
+ */
+ public void setShortcutAction(String shortcutAction) {
+ this.mShortcutAction = shortcutAction;
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ if (!isLegacyCompatibilityMode()) {
+ PhoneNumberListAdapter adapter = (PhoneNumberListAdapter)getAdapter();
+ pickPhoneNumber(adapter.getDataUri(position));
+ } else {
+ LegacyPhoneNumberListAdapter adapter = (LegacyPhoneNumberListAdapter)getAdapter();
+ pickPhoneNumber(adapter.getPhoneUri(position));
+ }
+ }
+
+ @Override
+ protected ContactEntryListAdapter createListAdapter() {
+ if (!isLegacyCompatibilityMode()) {
+ PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity());
+ adapter.setDisplayPhotos(true);
+ return adapter;
+ } else {
+ LegacyPhoneNumberListAdapter adapter = new LegacyPhoneNumberListAdapter(getActivity());
+ adapter.setDisplayPhotos(true);
+ return adapter;
+ }
+ }
+
+ @Override
+ protected void configureAdapter() {
+ setSectionHeaderDisplayEnabled(!isSearchMode());
+ setAizyEnabled(!isSearchMode());
+ super.configureAdapter();
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ return inflater.inflate(R.layout.contacts_list_content, null);
+ }
+
+ public void pickPhoneNumber(Uri uri) {
+ if (mShortcutAction == null) {
+ mListener.onPickPhoneNumberAction(uri);
+ } else {
+ if (isLegacyCompatibilityMode()) {
+ throw new UnsupportedOperationException();
+ }
+ ShortcutIntentBuilder builder = new ShortcutIntentBuilder(getActivity(), this);
+ builder.createPhoneNumberShortcutIntent(uri, mShortcutAction);
+ }
+ }
+
+ public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
+ mListener.onShortcutIntentCreated(shortcutIntent);
+ }
+
+ @Override
+ public void startSearch(String initialQuery) {
+ ContactsSearchManager.startSearchForResult(getActivity(), initialQuery,
+ ACTIVITY_REQUEST_CODE_FILTER, getContactsRequest());
+ }
+}
diff --git a/src/com/android/contacts/list/PostalAddressListAdapter.java b/src/com/android/contacts/list/PostalAddressListAdapter.java
new file mode 100644
index 0000000..797089c
--- /dev/null
+++ b/src/com/android/contacts/list/PostalAddressListAdapter.java
@@ -0,0 +1,175 @@
+/*
+ * 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.list;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A cursor adapter for the {@link StructuredPostal#CONTENT_TYPE} content type.
+ */
+public class PostalAddressListAdapter extends ContactEntryListAdapter {
+
+ static final String[] POSTALS_PROJECTION = new String[] {
+ StructuredPostal._ID, // 0
+ StructuredPostal.TYPE, // 1
+ StructuredPostal.LABEL, // 2
+ StructuredPostal.DATA, // 3
+ StructuredPostal.DISPLAY_NAME_PRIMARY, // 4
+ StructuredPostal.DISPLAY_NAME_ALTERNATIVE, // 5
+ StructuredPostal.PHOTO_ID, // 6
+ };
+
+ protected static final int POSTAL_ID_COLUMN_INDEX = 0;
+ protected static final int POSTAL_TYPE_COLUMN_INDEX = 1;
+ protected static final int POSTAL_LABEL_COLUMN_INDEX = 2;
+ protected static final int POSTAL_ADDRESS_COLUMN_INDEX = 3;
+ protected static final int POSTAL_PRIMARY_DISPLAY_NAME_COLUMN_INDEX = 4;
+ protected static final int POSTAL_ALTERNATIVE_DISPLAY_NAME_COLUMN_INDEX = 5;
+ protected static final int POSTAL_PHOTO_ID_COLUMN_INDEX = 6;
+
+ private CharSequence mUnknownNameText;
+ private int mDisplayNameColumnIndex;
+ private int mAlternativeDisplayNameColumnIndex;
+
+ public PostalAddressListAdapter(Context context) {
+ super(context);
+
+ mUnknownNameText = context.getText(android.R.string.unknownName);
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ loader.setUri(buildSectionIndexerUri(StructuredPostal.CONTENT_URI));
+ loader.setProjection(POSTALS_PROJECTION);
+
+ if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
+ loader.setSortOrder(StructuredPostal.SORT_KEY_PRIMARY);
+ } else {
+ loader.setSortOrder(StructuredPostal.SORT_KEY_ALTERNATIVE);
+ }
+ }
+
+ protected static Uri buildSectionIndexerUri(Uri uri) {
+ return uri.buildUpon()
+ .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
+ }
+
+ @Override
+ public String getContactDisplayName(int position) {
+ return ((Cursor)getItem(position)).getString(mDisplayNameColumnIndex);
+ }
+
+ @Override
+ public void setContactNameDisplayOrder(int displayOrder) {
+ super.setContactNameDisplayOrder(displayOrder);
+ if (getContactNameDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+ mDisplayNameColumnIndex = POSTAL_PRIMARY_DISPLAY_NAME_COLUMN_INDEX;
+ mAlternativeDisplayNameColumnIndex = POSTAL_ALTERNATIVE_DISPLAY_NAME_COLUMN_INDEX;
+ } else {
+ mDisplayNameColumnIndex = POSTAL_ALTERNATIVE_DISPLAY_NAME_COLUMN_INDEX;
+ mAlternativeDisplayNameColumnIndex = POSTAL_PRIMARY_DISPLAY_NAME_COLUMN_INDEX;
+ }
+ }
+
+ /**
+ * Builds a {@link Data#CONTENT_URI} for the current cursor
+ * position.
+ */
+ public Uri getDataUri(int position) {
+ long id = ((Cursor)getItem(position)).getLong(POSTAL_ID_COLUMN_INDEX);
+ return ContentUris.withAppendedId(Data.CONTENT_URI, id);
+ }
+
+ @Override
+ protected View newView(Context context, int partition, Cursor cursor, int position,
+ ViewGroup parent) {
+ final ContactListItemView view = new ContactListItemView(context, null);
+ view.setUnknownNameText(mUnknownNameText);
+ view.setTextWithHighlightingFactory(getTextWithHighlightingFactory());
+ return view;
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ ContactListItemView view = (ContactListItemView)itemView;
+ bindSectionHeaderAndDivider(view, position);
+ bindName(view, cursor);
+ bindPhoto(view, cursor);
+ bindPostalAddress(view, cursor);
+ }
+
+ protected void bindPostalAddress(ContactListItemView view, Cursor cursor) {
+ CharSequence label = null;
+ if (!cursor.isNull(POSTAL_TYPE_COLUMN_INDEX)) {
+ final int type = cursor.getInt(POSTAL_TYPE_COLUMN_INDEX);
+ final String customLabel = cursor.getString(POSTAL_LABEL_COLUMN_INDEX);
+
+ // TODO cache
+ label = StructuredPostal.getTypeLabel(getContext().getResources(), type, label);
+ }
+ view.setLabel(label);
+ view.showData(cursor, POSTAL_ADDRESS_COLUMN_INDEX);
+ }
+
+ protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
+ final int section = getSectionForPosition(position);
+ if (getPositionForSection(section) == position) {
+ String title = (String)getSections()[section];
+ view.setSectionHeader(title);
+ } else {
+ view.setDividerVisible(false);
+ view.setSectionHeader(null);
+ }
+
+ // move the divider for the last item in a section
+ if (getPositionForSection(section + 1) - 1 == position) {
+ view.setDividerVisible(false);
+ } else {
+ view.setDividerVisible(true);
+ }
+ }
+
+ protected void bindName(final ContactListItemView view, Cursor cursor) {
+ view.showDisplayName(cursor, mDisplayNameColumnIndex, isNameHighlightingEnabled(),
+ mAlternativeDisplayNameColumnIndex);
+// view.showPhoneticName(cursor, PHONE_PHONETIC_NAME_COLUMN_INDEX);
+ }
+
+ protected void bindPhoto(final ContactListItemView view, Cursor cursor) {
+ long photoId = 0;
+ if (!cursor.isNull(POSTAL_PHOTO_ID_COLUMN_INDEX)) {
+ photoId = cursor.getLong(POSTAL_PHOTO_ID_COLUMN_INDEX);
+ }
+
+ getPhotoLoader().loadPhoto(view.getPhotoView(), photoId);
+ }
+//
+// protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
+// view.showSnippet(cursor, SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX,
+// SUMMARY_SNIPPET_DATA1_COLUMN_INDEX, SUMMARY_SNIPPET_DATA4_COLUMN_INDEX);
+// }
+
+}
diff --git a/src/com/android/contacts/list/PostalAddressPickerFragment.java b/src/com/android/contacts/list/PostalAddressPickerFragment.java
new file mode 100644
index 0000000..6c429f2
--- /dev/null
+++ b/src/com/android/contacts/list/PostalAddressPickerFragment.java
@@ -0,0 +1,87 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Fragment containing a postal address list for picking.
+ */
+public class PostalAddressPickerFragment
+ extends ContactEntryListFragment<ContactEntryListAdapter> {
+ private OnPostalAddressPickerActionListener mListener;
+
+ public PostalAddressPickerFragment() {
+ setPhotoLoaderEnabled(true);
+ setSectionHeaderDisplayEnabled(true);
+ }
+
+ public void setOnPostalAddressPickerActionListener(
+ OnPostalAddressPickerActionListener listener) {
+ this.mListener = listener;
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ if (!isLegacyCompatibilityMode()) {
+ PostalAddressListAdapter adapter = (PostalAddressListAdapter)getAdapter();
+// if (adapter.isSearchAllContactsItemPosition(position)) {
+// searchAllContacts();
+// } else {
+ pickPostalAddress(adapter.getDataUri(position));
+// }
+ } else {
+ LegacyPostalAddressListAdapter adapter = (LegacyPostalAddressListAdapter)getAdapter();
+ pickPostalAddress(adapter.getContactMethodUri(position));
+ }
+ }
+
+ @Override
+ protected ContactEntryListAdapter createListAdapter() {
+ if (!isLegacyCompatibilityMode()) {
+ PostalAddressListAdapter adapter = new PostalAddressListAdapter(getActivity());
+ adapter.setSectionHeaderDisplayEnabled(true);
+ adapter.setDisplayPhotos(true);
+ return adapter;
+ } else {
+ LegacyPostalAddressListAdapter adapter =
+ new LegacyPostalAddressListAdapter(getActivity());
+ adapter.setSectionHeaderDisplayEnabled(false);
+ adapter.setDisplayPhotos(false);
+ return adapter;
+ }
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ if (isSearchMode()) {
+ return inflater.inflate(R.layout.contacts_search_content, null);
+ } else if (isSearchResultsMode()) {
+ return inflater.inflate(R.layout.contacts_list_search_results, null);
+ } else {
+ return inflater.inflate(R.layout.contacts_list_content, null);
+ }
+ }
+
+ public void pickPostalAddress(Uri uri) {
+ mListener.onPickPostalAddressAction(uri);
+ }
+}
diff --git a/src/com/android/contacts/list/ProviderStatusLoader.java b/src/com/android/contacts/list/ProviderStatusLoader.java
new file mode 100644
index 0000000..2eb9672
--- /dev/null
+++ b/src/com/android/contacts/list/ProviderStatusLoader.java
@@ -0,0 +1,211 @@
+/*
+ * 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.list;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.ContactsContract.ProviderStatus;
+
+/**
+ * Checks provider status and configures a list adapter accordingly.
+ */
+public class ProviderStatusLoader {
+
+ private final Context mContext;
+
+ public ProviderStatusLoader(Context context) {
+ this.mContext = context;
+ }
+
+ public int getProviderStatus() {
+ // This query can be performed on the UI thread because
+ // the API explicitly allows such use.
+ Cursor cursor = mContext.getContentResolver().query(
+ ProviderStatus.CONTENT_URI,
+ new String[] { ProviderStatus.STATUS, ProviderStatus.DATA1 }, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ return cursor.getInt(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ return ProviderStatus.STATUS_NORMAL;
+ }
+
+
+// View importFailureView = findViewById(R.id.import_failure);
+// if (importFailureView == null) {
+// return true;
+// }
+//
+// TextView messageView = (TextView) findViewById(R.id.emptyText);
+//
+// // 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);
+// 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_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;
+// }
+// }
+// }
+// } finally {
+// cursor.close();
+// }
+// }
+//
+// importFailureView.setVisibility(
+// mProviderStatus == ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY
+// ? View.VISIBLE
+// : View.GONE);
+// return mProviderStatus == ProviderStatus.STATUS_NORMAL;
+//}
+
+//
+// /**
+// * Obtains the contacts provider status and configures the UI accordingly.
+// *
+// * @param loadData true if the method needs to start a query when the
+// * provider is in the normal state
+// * @return true if the provider status is normal
+// */
+// private boolean checkProviderState(boolean loadData) {
+// View importFailureView = findViewById(R.id.import_failure);
+// if (importFailureView == null) {
+// return true;
+// }
+//
+// TextView messageView = (TextView) findViewById(R.id.emptyText);
+//
+// // 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);
+// 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_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;
+// }
+// }
+// }
+// } finally {
+// cursor.close();
+// }
+// }
+//
+// importFailureView.setVisibility(
+// mProviderStatus == ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY
+// ? View.VISIBLE
+// : View.GONE);
+// return mProviderStatus == ProviderStatus.STATUS_NORMAL;
+// }
+//
+// private void configureImportFailureView(View importFailureView) {
+//
+// OnClickListener listener = new OnClickListener(){
+//
+// public void onClick(View v) {
+// switch(v.getId()) {
+// case R.id.import_failure_uninstall_apps: {
+// startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
+// break;
+// }
+// case R.id.import_failure_retry_upgrade: {
+// // Send a provider status update, which will trigger a retry
+// ContentValues values = new ContentValues();
+// values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING);
+// getContentResolver().update(ProviderStatus.CONTENT_URI, values, null, null);
+// break;
+// }
+// }
+// }};
+//
+// Button uninstallApps = (Button) findViewById(R.id.import_failure_uninstall_apps);
+// uninstallApps.setOnClickListener(listener);
+//
+// Button retryUpgrade = (Button) findViewById(R.id.import_failure_retry_upgrade);
+// retryUpgrade.setOnClickListener(listener);
+// }
+
+}
diff --git a/src/com/android/contacts/list/ShortcutIntentBuilder.java b/src/com/android/contacts/list/ShortcutIntentBuilder.java
new file mode 100644
index 0000000..7a4f9de
--- /dev/null
+++ b/src/com/android/contacts/list/ShortcutIntentBuilder.java
@@ -0,0 +1,397 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+import com.android.contacts.util.Constants;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+
+import java.util.Random;
+
+/**
+ * Constructs shortcut intents.
+ */
+public class ShortcutIntentBuilder {
+
+ private static final String[] CONTACT_COLUMNS = {
+ Contacts.DISPLAY_NAME,
+ Contacts.PHOTO_ID,
+ };
+
+ private static final int CONTACT_DISPLAY_NAME_COLUMN_INDEX = 0;
+ private static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 1;
+
+ private static final String[] PHONE_COLUMNS = {
+ Phone.DISPLAY_NAME,
+ Phone.PHOTO_ID,
+ Phone.NUMBER,
+ Phone.TYPE,
+ };
+
+ private static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 0;
+ private static final int PHONE_PHOTO_ID_COLUMN_INDEX = 1;
+ private static final int PHONE_NUMBER_COLUMN_INDEX = 2;
+ private static final int PHONE_TYPE_COLUMN_INDEX = 3;
+
+ private static final String[] PHOTO_COLUMNS = {
+ Photo.PHOTO,
+ };
+
+ private static final int PHOTO_PHOTO_COLUMN_INDEX = 0;
+
+ private static final String PHOTO_SELECTION = Photo._ID + "=?";
+
+ private final OnShortcutIntentCreatedListener mListener;
+ private final Context mContext;
+ private final int mIconSize;
+
+ /**
+ * Listener interface.
+ */
+ public interface OnShortcutIntentCreatedListener {
+
+ /**
+ * Callback for shortcut intent creation.
+ *
+ * @param uri the original URI for which the shortcut intent has been
+ * created.
+ * @param shortcutIntent resulting shortcut intent.
+ */
+ void onShortcutIntentCreated(Uri uri, Intent shortcutIntent);
+ }
+
+ public ShortcutIntentBuilder(Context context, OnShortcutIntentCreatedListener listener) {
+ mContext = context;
+ mListener = listener;
+
+ mIconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
+ }
+
+ public void createContactShortcutIntent(Uri contactUri) {
+ new ContactLoadingAsyncTask(contactUri).execute();
+ }
+
+ public void createPhoneNumberShortcutIntent(Uri dataUri, String shortcutAction) {
+ new PhoneNumberLoadingAsyncTask(dataUri, shortcutAction).execute();
+ }
+
+ /**
+ * An asynchronous task that loads name, photo and other data from the database.
+ */
+ private abstract class LoadingAsyncTask extends AsyncTask<Void, Void, Void> {
+ protected Uri mUri;
+ protected String mDisplayName;
+ protected byte[] mBitmapData;
+ protected long mPhotoId;
+
+ public LoadingAsyncTask(Uri uri) {
+ mUri = uri;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ loadData();
+ loadPhoto();
+ return null;
+ }
+
+ protected abstract void loadData();
+
+ private void loadPhoto() {
+ if (mPhotoId == 0) {
+ return;
+ }
+
+ ContentResolver resolver = mContext.getContentResolver();
+ Cursor cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLUMNS, PHOTO_SELECTION,
+ new String[] { String.valueOf(mPhotoId) }, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ mBitmapData = cursor.getBlob(PHOTO_PHOTO_COLUMN_INDEX);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+ }
+
+ private final class ContactLoadingAsyncTask extends LoadingAsyncTask {
+ public ContactLoadingAsyncTask(Uri uri) {
+ super(uri);
+ }
+
+ @Override
+ protected void loadData() {
+ ContentResolver resolver = mContext.getContentResolver();
+ Cursor cursor = resolver.query(mUri, CONTACT_COLUMNS, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ mDisplayName = cursor.getString(CONTACT_DISPLAY_NAME_COLUMN_INDEX);
+ mPhotoId = cursor.getLong(CONTACT_PHOTO_ID_COLUMN_INDEX);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+ @Override
+ protected void onPostExecute(Void result) {
+ createContactShortcutIntent(mUri, mDisplayName, mBitmapData);
+ }
+ }
+
+ private final class PhoneNumberLoadingAsyncTask extends LoadingAsyncTask {
+ private final String mShortcutAction;
+ private String mPhoneNumber;
+ private int mPhoneType;
+
+ public PhoneNumberLoadingAsyncTask(Uri uri, String shortcutAction) {
+ super(uri);
+ mShortcutAction = shortcutAction;
+ }
+
+ @Override
+ protected void loadData() {
+ ContentResolver resolver = mContext.getContentResolver();
+ Cursor cursor = resolver.query(mUri, PHONE_COLUMNS, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ mDisplayName = cursor.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX);
+ mPhotoId = cursor.getLong(PHONE_PHOTO_ID_COLUMN_INDEX);
+ mPhoneNumber = cursor.getString(PHONE_NUMBER_COLUMN_INDEX);
+ mPhoneType = cursor.getInt(PHONE_TYPE_COLUMN_INDEX);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ createPhoneNumberShortcutIntent(mUri, mDisplayName, mBitmapData, mPhoneNumber,
+ mPhoneType, mShortcutAction);
+ }
+ }
+
+ private void createContactShortcutIntent(Uri contactUri, String displayName,
+ byte[] bitmapData) {
+ Bitmap bitmap;
+ if (bitmapData != null) {
+ bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length, null);
+ } else {
+ final int[] fallbacks = {
+ R.drawable.ic_contact_picture,
+ R.drawable.ic_contact_picture_2,
+ R.drawable.ic_contact_picture_3
+ };
+ bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+ fallbacks[new Random().nextInt(fallbacks.length)]);
+ }
+
+ Intent shortcutIntent;
+ // This is a simple shortcut to view a contact.
+ shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT);
+ shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
+ Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+
+ shortcutIntent.setData(contactUri);
+ shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_MODE,
+ ContactsContract.QuickContact.MODE_LARGE);
+ shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES,
+ (String[]) null);
+ shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ final Bitmap icon = scaleToAppIconSize(framePhoto(bitmap));
+
+ Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
+
+ mListener.onShortcutIntentCreated(contactUri, intent);
+ }
+
+ private void createPhoneNumberShortcutIntent(Uri uri, String displayName, byte[] bitmapData,
+ String phoneNumber, int phoneType, String shortcutAction) {
+ Bitmap bitmap = null;
+ if (bitmapData != null) {
+ bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length, null);
+ }
+
+ Uri phoneUri;
+ if (Intent.ACTION_CALL.equals(shortcutAction)) {
+ // Make the URI a direct tel: URI so that it will always continue to work
+ phoneUri = Uri.fromParts(Constants.SCHEME_TEL, phoneNumber, null);
+ bitmap = generatePhoneNumberIcon(bitmap, phoneType, R.drawable.badge_action_call);
+ } else {
+ phoneUri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
+ bitmap = generatePhoneNumberIcon(bitmap, phoneType, R.drawable.badge_action_sms);
+ }
+
+ Intent shortcutIntent = new Intent(shortcutAction, phoneUri);
+ shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bitmap);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
+
+ mListener.onShortcutIntentCreated(uri, intent);
+ }
+
+ private Bitmap framePhoto(Bitmap photo) {
+ final Resources r = mContext.getResources();
+ final Drawable frame = r.getDrawable(com.android.internal.R.drawable.quickcontact_badge);
+
+ final int width = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_width);
+ final int height = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_height);
+
+ frame.setBounds(0, 0, width, height);
+
+ final Rect padding = new Rect();
+ frame.getPadding(padding);
+
+ final Rect source = new Rect(0, 0, photo.getWidth(), photo.getHeight());
+ final Rect destination = new Rect(padding.left, padding.top,
+ width - padding.right, height - padding.bottom);
+
+ final int d = Math.max(width, height);
+ final Bitmap b = Bitmap.createBitmap(d, d, Bitmap.Config.ARGB_8888);
+ final Canvas c = new Canvas(b);
+
+ c.translate((d - width) / 2.0f, (d - height) / 2.0f);
+ frame.draw(c);
+ c.drawBitmap(photo, source, destination, new Paint(Paint.FILTER_BITMAP_FLAG));
+
+ return b;
+ }
+
+ private Bitmap scaleToAppIconSize(Bitmap photo) {
+
+ // Setup the drawing classes
+ Bitmap icon = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(icon);
+
+ // Copy in the photo
+ Paint photoPaint = new Paint();
+ photoPaint.setDither(true);
+ photoPaint.setFilterBitmap(true);
+ Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
+ Rect dst = new Rect(0,0, mIconSize, mIconSize);
+ canvas.drawBitmap(photo, src, dst, photoPaint);
+
+ return icon;
+ }
+
+ /**
+ * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
+ * number, and if there is a photo also adds the call action icon.
+ */
+ private Bitmap generatePhoneNumberIcon(Bitmap photo, int phoneType, int actionResId) {
+ final Resources r = mContext.getResources();
+ boolean drawPhoneOverlay = true;
+ final float scaleDensity = r.getDisplayMetrics().scaledDensity;
+
+ Bitmap phoneIcon = ((BitmapDrawable) r.getDrawable(actionResId)).getBitmap();
+
+ // If there isn't a photo use the generic phone action icon instead
+ if (photo == null) {
+ photo = phoneIcon;
+ drawPhoneOverlay = false;
+ }
+
+ // Setup the drawing classes
+ Bitmap icon = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(icon);
+
+ // Copy in the photo
+ Paint photoPaint = new Paint();
+ photoPaint.setDither(true);
+ photoPaint.setFilterBitmap(true);
+ Rect src = new Rect(0, 0, photo.getWidth(), photo.getHeight());
+ Rect dst = new Rect(0, 0, mIconSize, mIconSize);
+ canvas.drawBitmap(photo, src, dst, photoPaint);
+
+ // Create an overlay for the phone number type
+ String overlay = null;
+ switch (phoneType) {
+ case Phone.TYPE_HOME:
+ overlay = mContext.getString(R.string.type_short_home);
+ break;
+
+ case Phone.TYPE_MOBILE:
+ overlay = mContext.getString(R.string.type_short_mobile);
+ break;
+
+ case Phone.TYPE_WORK:
+ overlay = mContext.getString(R.string.type_short_work);
+ break;
+
+ case Phone.TYPE_PAGER:
+ overlay = mContext.getString(R.string.type_short_pager);
+ break;
+
+ case Phone.TYPE_OTHER:
+ overlay = mContext.getString(R.string.type_short_other);
+ break;
+ }
+
+ if (overlay != null) {
+ Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
+ textPaint.setTextSize(20.0f * scaleDensity);
+ textPaint.setTypeface(Typeface.DEFAULT_BOLD);
+ textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
+ textPaint.setShadowLayer(3f, 1, 1, r.getColor(R.color.textColorIconOverlayShadow));
+ canvas.drawText(overlay, 2 * scaleDensity, 16 * scaleDensity, textPaint);
+ }
+
+ // Draw the phone action icon as an overlay
+ if (drawPhoneOverlay) {
+ src.set(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
+ int iconWidth = icon.getWidth();
+ dst.set(iconWidth - ((int) (20 * scaleDensity)), -1,
+ iconWidth, ((int) (19 * scaleDensity)));
+ canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
+ }
+
+ return icon;
+ }
+}
diff --git a/src/com/android/contacts/list/StrequentContactListAdapter.java b/src/com/android/contacts/list/StrequentContactListAdapter.java
new file mode 100644
index 0000000..b5aec28
--- /dev/null
+++ b/src/com/android/contacts/list/StrequentContactListAdapter.java
@@ -0,0 +1,212 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
+
+/**
+ * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type loading
+ * a combination of starred and frequently contacted.
+ */
+public class StrequentContactListAdapter extends ContactListAdapter {
+
+ private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
+ private TextView mSeparatorView;
+ private OnClickListener mCallButtonListener;
+ private int mCallButtonId;
+ private boolean mStarredContactsIncluded;
+ private boolean mFrequentlyContactedContactsIncluded;
+
+ public StrequentContactListAdapter(Context context, int callButtonId) {
+ super(context);
+ mCallButtonId = callButtonId;
+ }
+
+ public void setCallButtonListener(OnClickListener callButtonListener) {
+ mCallButtonListener = callButtonListener;
+ }
+
+ public void setStarredContactsIncluded(boolean flag) {
+ mStarredContactsIncluded = flag;
+ }
+
+ public void setFrequentlyContactedContactsIncluded(boolean flag) {
+ mFrequentlyContactedContactsIncluded = flag;
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ String sortOrder = getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY
+ ? Contacts.SORT_KEY_PRIMARY
+ : Contacts.SORT_KEY_ALTERNATIVE;
+ if (mStarredContactsIncluded && mFrequentlyContactedContactsIncluded) {
+ loader.setUri(Contacts.CONTENT_STREQUENT_URI);
+ } else if (mStarredContactsIncluded) {
+ loader.setUri(Contacts.CONTENT_URI);
+ loader.setSelection(Contacts.STARRED + "!=0");
+ } else if (mFrequentlyContactedContactsIncluded) {
+ loader.setUri(Contacts.CONTENT_URI);
+ loader.setSelection(Contacts.TIMES_CONTACTED + " > 0");
+ sortOrder = Contacts.TIMES_CONTACTED + " DESC";
+ } else {
+ throw new UnsupportedOperationException("Neither StarredContactsIncluded nor "
+ + "FrequentlyContactedContactsIncluded is set");
+ }
+
+ loader.setProjection(PROJECTION);
+ loader.setSortOrder(sortOrder);
+ }
+
+ @Override
+ protected void invalidate() {
+ super.invalidate();
+
+ // Sometimes the adapter is invalidated without calling changeCursor,
+ // need to reset the separator position then.
+ mFrequentSeparatorPos = ListView.INVALID_POSITION;
+ }
+
+ @Override
+ public void changeCursor(int partition, Cursor cursor) {
+ super.changeCursor(partition, cursor);
+
+ // Get the split between starred and frequent items, if the mode is strequent
+ mFrequentSeparatorPos = ListView.INVALID_POSITION;
+
+ if (mStarredContactsIncluded && mFrequentlyContactedContactsIncluded) {
+ int count = 0;
+ if (cursor != null && (count = cursor.getCount()) > 0) {
+ cursor.moveToPosition(-1);
+ for (int i = 0; cursor.moveToNext(); i++) {
+ int starred = cursor.getInt(CONTACT_STARRED_COLUMN_INDEX);
+ if (starred == 0) {
+ if (i > 0) {
+ // Only add the separator when there are starred items present
+ mFrequentSeparatorPos = i;
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getCount() {
+ if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
+ return super.getCount();
+ } else {
+ // Add a row for the separator
+ return super.getCount() + 1;
+ }
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return mFrequentSeparatorPos == ListView.INVALID_POSITION;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return position != mFrequentSeparatorPos;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ if (mFrequentSeparatorPos == ListView.INVALID_POSITION
+ || position < mFrequentSeparatorPos) {
+ return super.getItem(position);
+ } else {
+ return super.getItem(position - 1);
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (mFrequentSeparatorPos == ListView.INVALID_POSITION
+ || position < mFrequentSeparatorPos) {
+ return super.getItemId(position);
+ } else {
+ return super.getItemId(position - 1);
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (mFrequentSeparatorPos == ListView.INVALID_POSITION
+ || position < mFrequentSeparatorPos) {
+ return super.getItemViewType(position);
+ } else if (position == mFrequentSeparatorPos) {
+ return IGNORE_ITEM_VIEW_TYPE;
+ } else {
+ return super.getItemViewType(position - 1);
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (mFrequentSeparatorPos == ListView.INVALID_POSITION
+ || position < mFrequentSeparatorPos) {
+ return super.getView(position, convertView, parent);
+ } else if (position == mFrequentSeparatorPos) {
+ if (mSeparatorView == null) {
+ mSeparatorView = (TextView)LayoutInflater.from(getContext()).
+ inflate(R.layout.list_separator, parent, false);
+ mSeparatorView.setText(R.string.favoritesFrquentSeparator);
+ }
+ return mSeparatorView;
+ } else {
+ return super.getView(position - 1, convertView, parent);
+ }
+ }
+
+ @Override
+ protected View newView(Context context, int partition, Cursor cursor, int position,
+ ViewGroup parent) {
+ ContactListItemView view = (ContactListItemView)super.newView(context, partition, cursor,
+ position, parent);
+ view.setOnCallButtonClickListener(mCallButtonListener);
+ return view;
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ final ContactListItemView view = (ContactListItemView)itemView;
+
+ bindName(view, cursor);
+ bindQuickContact(view, cursor);
+ bindPresence(view, cursor);
+
+ // Make the call button visible if requested.
+ if (getHasPhoneNumber(position)) {
+ view.showCallButton(mCallButtonId, position);
+ } else {
+ view.hideCallButton();
+ }
+ }
+}
diff --git a/src/com/android/contacts/list/StrequentContactListFragment.java b/src/com/android/contacts/list/StrequentContactListFragment.java
new file mode 100644
index 0000000..4b8e2bb
--- /dev/null
+++ b/src/com/android/contacts/list/StrequentContactListFragment.java
@@ -0,0 +1,108 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+
+/**
+ * Fragment containing a list of starred contacts followed by a list of frequently contacted.
+ */
+public class StrequentContactListFragment extends ContactBrowseListFragment
+ implements OnClickListener {
+
+ private static final int CALL_BUTTON_ID = android.R.id.button1;
+
+ private boolean mStarredContactsIncluded = true;
+ private boolean mFrequentlyContactedContactsIncluded = true;
+
+ public StrequentContactListFragment() {
+ setSectionHeaderDisplayEnabled(false);
+ setPhotoLoaderEnabled(true);
+ }
+
+ @Override
+ protected boolean isNameHighlighingEnabled() {
+ // Since the list is not ordered alphabetically, we don't need to highlight the part
+ // that is used for sorting.
+ return false;
+ }
+
+ public void setStarredContactsIncluded(boolean flag) {
+ mStarredContactsIncluded = flag;
+ configureAdapter();
+ }
+
+ public void setFrequentlyContactedContactsIncluded(boolean flag) {
+ mFrequentlyContactedContactsIncluded = flag;
+ configureAdapter();
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ ContactListAdapter adapter = getAdapter();
+ viewContact(adapter.getContactUri(position));
+ }
+
+ @Override
+ protected ContactListAdapter createListAdapter() {
+ StrequentContactListAdapter adapter =
+ new StrequentContactListAdapter(getActivity(), CALL_BUTTON_ID);
+ adapter.setSectionHeaderDisplayEnabled(false);
+ adapter.setDisplayPhotos(true);
+ adapter.setQuickContactEnabled(true);
+ adapter.setCallButtonListener(this);
+
+ return adapter;
+ }
+
+ @Override
+ protected void configureAdapter() {
+ super.configureAdapter();
+
+ StrequentContactListAdapter adapter = (StrequentContactListAdapter)getAdapter();
+ if (adapter != null) {
+ adapter.setStarredContactsIncluded(mStarredContactsIncluded);
+ adapter.setFrequentlyContactedContactsIncluded(mFrequentlyContactedContactsIncluded);
+ }
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ return inflater.inflate(R.layout.contacts_list_content, null);
+ }
+
+ @Override
+ protected void prepareEmptyView() {
+ setEmptyText(R.string.noFavoritesHelpText);
+ }
+
+ public void onClick(View v) {
+ int id = v.getId();
+ switch (id) {
+ case CALL_BUTTON_ID: {
+ final int position = (Integer)v.getTag();
+ ContactListAdapter adapter = getAdapter();
+ callContact(adapter.getContactUri(position));
+ break;
+ }
+ }
+ }
+}
diff --git a/src/com/android/contacts/model/FallbackSource.java b/src/com/android/contacts/model/FallbackSource.java
index 9cc855c..a23426c 100644
--- a/src/com/android/contacts/model/FallbackSource.java
+++ b/src/com/android/contacts/model/FallbackSource.java
@@ -107,6 +107,8 @@
if (kind == null) {
kind = addKind(new DataKind(StructuredName.CONTENT_ITEM_TYPE,
R.string.nameLabelsGroup, -1, -1, true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
}
if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
diff --git a/src/com/android/contacts/model/GoogleSource.java b/src/com/android/contacts/model/GoogleSource.java
index 90abc92..8786fcb 100644
--- a/src/com/android/contacts/model/GoogleSource.java
+++ b/src/com/android/contacts/model/GoogleSource.java
@@ -157,113 +157,6 @@
return super.inflateWebsite(context, inflateLevel);
}
- // TODO: this should come from resource in the future
- // Note that frameworks/base/core/java/android/pim/vcard/VCardEntry.java also wants
- // this String.
- private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts";
-
- public static final void attemptMyContactsMembership(EntityDelta state, Context context) {
- final ValuesDelta stateValues = state.getValues();
- stateValues.setFromTemplate(true);
- final String accountName = stateValues.getAsString(RawContacts.ACCOUNT_NAME);
- final String accountType = stateValues.getAsString(RawContacts.ACCOUNT_TYPE);
- attemptMyContactsMembership(state, accountName, accountType, context, true);
- }
-
- public static final void createMyContactsIfNotExist(Account account, Context context) {
- attemptMyContactsMembership(null, account.name, account.type, context, true);
- }
-
- /**
- *
- * @param allowRecur If the group is created between querying/about to create, we recur. But
- * to prevent excess recursion, we provide a flag to make sure we only do the recursion loop
- * once
- */
- private static final void attemptMyContactsMembership(EntityDelta state,
- final String accountName, final String accountType, Context context,
- boolean allowRecur) {
- final ContentResolver resolver = context.getContentResolver();
-
- Cursor cursor = resolver.query(Groups.CONTENT_URI,
- new String[] {Groups.TITLE, Groups.SOURCE_ID, Groups.SHOULD_SYNC},
- Groups.ACCOUNT_NAME + " =? AND " + Groups.ACCOUNT_TYPE + " =?",
- new String[] {accountName, accountType}, null);
-
- boolean myContactsExists = false;
- long assignToGroupSourceId = -1;
- while (cursor.moveToNext()) {
- if (GOOGLE_MY_CONTACTS_GROUP.equals(cursor.getString(0))) {
- myContactsExists = true;
- }
- if (assignToGroupSourceId == -1 && cursor.getInt(2) != 0) {
- assignToGroupSourceId = cursor.getInt(1);
- }
-
- if (myContactsExists && assignToGroupSourceId != -1) {
- break;
- }
- }
-
- if (myContactsExists && state == null) {
- return;
- }
-
- try {
- final ContentValues values = new ContentValues();
- values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
-
- if (!myContactsExists) {
- // create the group if it doesn't exist
- final ContentValues newGroup = new ContentValues();
- newGroup.put(Groups.TITLE, GOOGLE_MY_CONTACTS_GROUP);
-
- newGroup.put(Groups.ACCOUNT_NAME, accountName);
- newGroup.put(Groups.ACCOUNT_TYPE, accountType);
- newGroup.put(Groups.GROUP_VISIBLE, "1");
-
- ArrayList<ContentProviderOperation> operations =
- new ArrayList<ContentProviderOperation>();
-
- operations.add(ContentProviderOperation
- .newAssertQuery(Groups.CONTENT_URI)
- .withSelection(SELECTION_GROUPS_BY_TITLE_AND_ACCOUNT,
- new String[] {GOOGLE_MY_CONTACTS_GROUP, accountName, accountType})
- .withExpectedCount(0).build());
- operations.add(ContentProviderOperation
- .newInsert(Groups.CONTENT_URI)
- .withValues(newGroup)
- .build());
- try {
- ContentProviderResult[] results = resolver.applyBatch(
- ContactsContract.AUTHORITY, operations);
- values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(results[1].uri));
- } catch (RemoteException e) {
- throw new IllegalStateException("Problem querying for groups", e);
- } catch (OperationApplicationException e) {
- // the group was created after the query but before we tried to create it
- if (allowRecur) {
- attemptMyContactsMembership(
- state, accountName, accountType, context, false);
- }
- return;
- }
- } else {
- if (assignToGroupSourceId != -1) {
- values.put(GroupMembership.GROUP_SOURCE_ID, assignToGroupSourceId);
- } else {
- // there are no Groups to add this contact to, so don't apply any membership
- // TODO: alert user that their contact will be dropped?
- }
- }
- if (state != null) {
- state.addEntry(ValuesDelta.fromAfter(values));
- }
- } finally {
- cursor.close();
- }
- }
-
@Override
public int getHeaderColor(Context context) {
return 0xff89c2c2;
diff --git a/src/com/android/contacts/ui/ContactsPreferencesActivity.java b/src/com/android/contacts/ui/ContactsPreferencesActivity.java
index 5a89745..8e0d94e 100644
--- a/src/com/android/contacts/ui/ContactsPreferencesActivity.java
+++ b/src/com/android/contacts/ui/ContactsPreferencesActivity.java
@@ -23,6 +23,7 @@
import com.android.contacts.model.Sources;
import com.android.contacts.model.EntityDelta.ValuesDelta;
import com.android.contacts.util.EmptyService;
+import com.android.contacts.util.LocalizedNameResolver;
import com.android.contacts.util.WeakAsyncTask;
import com.google.android.collect.Lists;
@@ -441,8 +442,17 @@
put(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, visible ? 1 : 0);
}
+ private String getAccountType() {
+ return (mBefore == null ? mAfter : mBefore).getAsString(Settings.ACCOUNT_TYPE);
+ }
+
public CharSequence getTitle(Context context) {
if (mUngrouped) {
+ final String customAllContactsName =
+ LocalizedNameResolver.getAllContactsName(context, getAccountType());
+ if (customAllContactsName != null) {
+ return customAllContactsName;
+ }
if (mAccountHasGroups) {
return context.getText(R.string.display_ungrouped);
} else {
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index c70cff6..7b61e19 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;
@@ -32,9 +32,9 @@
import com.android.contacts.model.EntityDelta.ValuesDelta;
import com.android.contacts.ui.widget.BaseContactEditorView;
import com.android.contacts.ui.widget.PhotoEditorView;
+import com.android.contacts.util.DialogManager;
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;
}
/**
@@ -1315,12 +1319,6 @@
EntityModifier.ensureKindExists(insert, source, Phone.CONTENT_ITEM_TYPE);
EntityModifier.ensureKindExists(insert, source, Email.CONTENT_ITEM_TYPE);
- // Create "My Contacts" membership for Google contacts
- // TODO: move this off into "templates" for each given source
- if (GoogleSource.ACCOUNT_TYPE.equals(source.accountType)) {
- GoogleSource.attemptMyContactsMembership(insert, this);
- }
-
if (mState == null) {
// Create state if none exists yet
mState = EntitySet.fromSingle(insert);
@@ -1409,4 +1407,8 @@
ContactsSearchManager.startSearch(this, initialQuery);
}
}
+
+ public DialogManager getDialogManager() {
+ return mDialogManager;
+ }
}
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index 83bf2fb..0916bc0 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -25,13 +25,15 @@
import com.android.contacts.model.Editor.EditorListener;
import com.android.contacts.model.EntityDelta.ValuesDelta;
import com.android.contacts.ui.ViewIdGenerator;
+import com.android.contacts.util.DialogManager;
+import com.android.contacts.util.DialogManager.DialogShowingView;
+import android.app.AlertDialog;
+import android.app.Dialog;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Entity;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.os.Parcel;
-import android.os.Parcelable;
+import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
@@ -39,49 +41,46 @@
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.text.TextUtils;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.View.OnClickListener;
+import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
+import java.util.ArrayList;
+
/**
* Custom view that provides all the editor interaction for a specific
* {@link Contacts} represented through an {@link EntityDelta}. Callers can
* reuse this view and quickly rebuild its contents through
- * {@link #setState(EntityDelta, ContactsSource)}.
+ * {@link #setState(EntityDelta, ContactsSource, ViewIdGenerator)}.
* <p>
* Internal updates are performed against {@link ValuesDelta} so that the
* source {@link Entity} can be swapped out. Any state-based changes, such as
* adding {@link Data} rows or changing {@link EditType}, are performed through
* {@link EntityModifier} to ensure that {@link ContactsSource} are enforced.
*/
-public class ContactEditorView extends BaseContactEditorView implements OnClickListener {
- private TextView mReadOnly;
- private TextView mReadOnlyName;
-
+public class ContactEditorView extends BaseContactEditorView implements DialogShowingView {
private View mPhotoStub;
private GenericEditorView mName;
- private boolean mIsSourceReadOnly;
- private ViewGroup mGeneral;
- private ViewGroup mSecondary;
- private boolean mSecondaryVisible;
+ private ViewGroup mFields;
- private TextView mSecondaryHeader;
-
- private Drawable mSecondaryOpen;
- private Drawable mSecondaryClosed;
-
- private View mHeaderColorBar;
- private View mSideBar;
private ImageView mHeaderIcon;
private TextView mHeaderAccountType;
private TextView mHeaderAccountName;
+ private Button mAddFieldButton;
+
private long mRawContactId = -1;
+ private DialogManager mDialogManager = null;
+
+ private static final String DIALOG_ID_KEY = "dialog_id";
+ private static final int DIALOG_ID_FIELD_SELECTOR = 1;
+
public ContactEditorView(Context context) {
super(context);
}
@@ -95,66 +94,29 @@
protected void onFinishInflate() {
super.onFinishInflate();
- mInflater = (LayoutInflater)getContext().getSystemService(
- Context.LAYOUT_INFLATER_SERVICE);
+ mInflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mPhoto = (PhotoEditorView)findViewById(R.id.edit_photo);
mPhotoStub = findViewById(R.id.stub_photo);
final int photoSize = getResources().getDimensionPixelSize(R.dimen.edit_photo_size);
- mReadOnly = (TextView)findViewById(R.id.edit_read_only);
-
mName = (GenericEditorView)findViewById(R.id.edit_name);
mName.setMinimumHeight(photoSize);
mName.setDeletable(false);
- mReadOnlyName = (TextView) findViewById(R.id.read_only_name);
+ mFields = (ViewGroup)findViewById(R.id.sect_fields);
- mGeneral = (ViewGroup)findViewById(R.id.sect_general);
- mSecondary = (ViewGroup)findViewById(R.id.sect_secondary);
-
- mHeaderColorBar = findViewById(R.id.header_color_bar);
- mSideBar = findViewById(R.id.color_bar);
mHeaderIcon = (ImageView) findViewById(R.id.header_icon);
mHeaderAccountType = (TextView) findViewById(R.id.header_account_type);
mHeaderAccountName = (TextView) findViewById(R.id.header_account_name);
- mSecondaryHeader = (TextView)findViewById(R.id.head_secondary);
- mSecondaryHeader.setOnClickListener(this);
-
- final Resources res = getResources();
- mSecondaryOpen = res.getDrawable(com.android.internal.R.drawable.expander_ic_maximized);
- mSecondaryClosed = res.getDrawable(com.android.internal.R.drawable.expander_ic_minimized);
-
- this.setSecondaryVisible(false);
- }
-
- /** {@inheritDoc} */
- public void onClick(View v) {
- // Toggle visibility of secondary kinds
- final boolean makeVisible = mSecondary.getVisibility() != View.VISIBLE;
- this.setSecondaryVisible(makeVisible);
- }
-
- /**
- * Set the visibility of secondary sections, along with header icon.
- *
- * <p>If the source is read-only and there's no secondary fields, the entire secondary section
- * will be hidden.
- */
- private void setSecondaryVisible(boolean makeVisible) {
- mSecondaryVisible = makeVisible;
-
- if (!mIsSourceReadOnly && mSecondary.getChildCount() > 0) {
- mSecondaryHeader.setVisibility(View.VISIBLE);
- mSecondaryHeader.setCompoundDrawablesWithIntrinsicBounds(
- makeVisible ? mSecondaryOpen : mSecondaryClosed, null, null, null);
- mSecondary.setVisibility(makeVisible ? View.VISIBLE : View.GONE);
- } else {
- mSecondaryHeader.setVisibility(View.GONE);
- mSecondary.setVisibility(View.GONE);
- }
+ mAddFieldButton = (Button) findViewById(R.id.button_add_field);
+ mAddFieldButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ showDialog(DIALOG_ID_FIELD_SELECTOR);
+ }
+ });
}
/**
@@ -165,16 +127,13 @@
@Override
public void setState(EntityDelta state, ContactsSource source, ViewIdGenerator vig) {
// Remove any existing sections
- mGeneral.removeAllViews();
- mSecondary.removeAllViews();
+ mFields.removeAllViews();
// Bail if invalid state or source
if (state == null || source == null) return;
setId(vig.getId(state, null, null, ViewIdGenerator.NO_VIEW_INDEX));
- mIsSourceReadOnly = source.readOnly;
-
// Make sure we have StructuredName
EntityModifier.ensureKindExists(state, source, StructuredName.CONTENT_ITEM_TYPE);
@@ -198,24 +157,13 @@
EntityModifier.ensureKindExists(state, source, Photo.CONTENT_ITEM_TYPE);
mHasPhotoEditor = (source.getKindForMimetype(Photo.CONTENT_ITEM_TYPE) != null);
mPhoto.setVisibility(mHasPhotoEditor ? View.VISIBLE : View.GONE);
- mPhoto.setEnabled(!mIsSourceReadOnly);
- mName.setEnabled(!mIsSourceReadOnly);
+ mPhoto.setEnabled(true);
+ mName.setEnabled(true);
// Show and hide the appropriate views
- if (mIsSourceReadOnly) {
- mGeneral.setVisibility(View.GONE);
- mName.setVisibility(View.GONE);
- mReadOnly.setVisibility(View.VISIBLE);
- mReadOnly.setText(mContext.getString(R.string.contact_read_only, accountType));
- mReadOnlyName.setVisibility(View.VISIBLE);
- } else {
- mGeneral.setVisibility(View.VISIBLE);
- mName.setVisibility(View.VISIBLE);
- mReadOnly.setVisibility(View.GONE);
- mReadOnlyName.setVisibility(View.GONE);
- }
+ mFields.setVisibility(View.VISIBLE);
+ mName.setVisibility(View.VISIBLE);
- boolean anySecondaryFieldFilled = false;
// Create editor sections for each possible data kind
for (DataKind kind : source.getSortedDataKinds()) {
// Skip kind of not editable
@@ -225,36 +173,21 @@
if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
// Handle special case editor for structured name
final ValuesDelta primary = state.getPrimaryEntry(mimeType);
- if (!mIsSourceReadOnly) {
- mName.setValues(kind, primary, state, mIsSourceReadOnly, vig);
- } else {
- String displayName = primary.getAsString(StructuredName.DISPLAY_NAME);
- mReadOnlyName.setText(displayName);
- }
+ mName.setValues(kind, primary, state, false, vig);
} else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
// Handle special case editor for photos
final ValuesDelta primary = state.getPrimaryEntry(mimeType);
- mPhoto.setValues(kind, primary, state, mIsSourceReadOnly, vig);
- if (mIsSourceReadOnly && !mPhoto.hasSetPhoto()) {
- mPhotoStub.setVisibility(View.GONE);
- } else {
- mPhotoStub.setVisibility(View.VISIBLE);
- }
- } else if (!mIsSourceReadOnly) {
+ mPhoto.setValues(kind, primary, state, false, vig);
+ mPhotoStub.setVisibility(View.VISIBLE);
+ } else {
// Otherwise use generic section-based editors
if (kind.fieldList == null) continue;
- final ViewGroup parent = kind.secondary ? mSecondary : mGeneral;
final KindSectionView section = (KindSectionView)mInflater.inflate(
- R.layout.item_kind_section, parent, false);
- section.setState(kind, state, mIsSourceReadOnly, vig);
- if (kind.secondary && section.isAnyEditorFilledOut()) {
- anySecondaryFieldFilled = true;
- }
- parent.addView(section);
+ R.layout.item_kind_section, mFields, false);
+ section.setState(kind, state, false, vig);
+ mFields.addView(section);
}
}
-
- setSecondaryVisible(anySecondaryFieldFilled);
}
/**
@@ -270,56 +203,53 @@
return mRawContactId;
}
- private static class SavedState extends BaseSavedState {
- public boolean mSecondaryVisible;
-
- SavedState(Parcelable superState) {
- super(superState);
- }
-
- private SavedState(Parcel in) {
- super(in);
- mSecondaryVisible = (in.readInt() == 0 ? false : true);
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- super.writeToParcel(out, flags);
- out.writeInt(mSecondaryVisible ? 1 : 0);
- }
-
- public static final Parcelable.Creator<SavedState> CREATOR
- = new Parcelable.Creator<SavedState>() {
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in);
- }
-
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
+ /* package */
+ void showDialog(int bundleDialogId) {
+ final Bundle bundle = new Bundle();
+ bundle.putInt(DIALOG_ID_KEY, bundleDialogId);
+ getDialogManager().showDialogInView(this, bundle);
}
- /**
- * Saves the visibility of the secondary field.
- */
- @Override
- protected Parcelable onSaveInstanceState() {
- Parcelable superState = super.onSaveInstanceState();
- SavedState ss = new SavedState(superState);
-
- ss.mSecondaryVisible = mSecondaryVisible;
- return ss;
+ 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;
}
- /**
- * Restores the visibility of the secondary field.
- */
- @Override
- protected void onRestoreInstanceState(Parcelable state) {
- SavedState ss = (SavedState) state;
- super.onRestoreInstanceState(ss.getSuperState());
-
- setSecondaryVisible(ss.mSecondaryVisible);
+ 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_FIELD_SELECTOR:
+ final ArrayList<CharSequence> items =
+ new ArrayList<CharSequence>(mFields.getChildCount());
+ for (int i = 0; i < mFields.getChildCount(); i++) {
+ final KindSectionView sectionView = (KindSectionView) mFields.getChildAt(i);
+ // not a list and already exists? ignore
+ if (!sectionView.getKind().isList && sectionView.getEditorCount() != 0) {
+ continue;
+ }
+ items.add(sectionView.getTitle());
+ }
+ final DialogInterface.OnClickListener itemClickListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ final KindSectionView view = (KindSectionView) mFields.getChildAt(which);
+ view.addItem();
+ }
+ };
+ return new AlertDialog.Builder(getContext())
+ .setItems(items.toArray(new CharSequence[0]), itemClickListener)
+ .create();
+ default:
+ throw new IllegalArgumentException("Invalid dialogId: " + dialogId);
+ }
}
}
diff --git a/src/com/android/contacts/ui/widget/DontPressWithParentImageView.java b/src/com/android/contacts/ui/widget/DontPressWithParentImageView.java
deleted file mode 100644
index bdb0e0a..0000000
--- a/src/com/android/contacts/ui/widget/DontPressWithParentImageView.java
+++ /dev/null
@@ -1,42 +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.ui.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ImageView;
-
-/**
- * Special class to to allow the parent to be pressed without being pressed itself.
- * This way the line of a tab can be pressed, but the image itself is not.
- */
-public class DontPressWithParentImageView extends ImageView {
-
- public DontPressWithParentImageView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public void setPressed(boolean pressed) {
- // If the parent is pressed, do not set to pressed.
- if (pressed && ((View) getParent()).isPressed()) {
- return;
- }
- super.setPressed(pressed);
- }
-}
diff --git a/src/com/android/contacts/ui/widget/GenericEditorView.java b/src/com/android/contacts/ui/widget/GenericEditorView.java
index 24262bb..5c2f9b7 100644
--- a/src/com/android/contacts/ui/widget/GenericEditorView.java
+++ b/src/com/android/contacts/ui/widget/GenericEditorView.java
@@ -26,12 +26,16 @@
import com.android.contacts.model.ContactsSource.EditType;
import com.android.contacts.model.EntityDelta.ValuesDelta;
import com.android.contacts.ui.ViewIdGenerator;
+import com.android.contacts.util.ViewGroupAnimator;
+import com.android.contacts.util.DialogManager;
+import com.android.contacts.util.DialogManager.DialogShowingView;
import android.app.AlertDialog;
import android.app.Dialog;
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;
@@ -47,6 +51,7 @@
import android.view.inputmethod.EditorInfo;
import android.widget.ArrayAdapter;
import android.widget.EditText;
+import android.widget.ImageButton;
import android.widget.ListAdapter;
import android.widget.RelativeLayout;
import android.widget.TextView;
@@ -58,10 +63,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
@@ -70,8 +80,7 @@
protected TextView mLabel;
protected ViewGroup mFields;
protected View mDelete;
- protected View mMore;
- protected View mLess;
+ protected ImageButton mMoreOrLess;
protected DataKind mKind;
protected ValuesDelta mEntry;
@@ -85,6 +94,7 @@
private EditType mPendingType;
private ViewIdGenerator mViewIdGenerator;
+ private DialogManager mDialogManager = null;
public GenericEditorView(Context context) {
super(context);
@@ -108,11 +118,8 @@
mDelete = findViewById(R.id.edit_delete);
mDelete.setOnClickListener(this);
- mMore = findViewById(R.id.edit_more);
- mMore.setOnClickListener(this);
-
- mLess = findViewById(R.id.edit_less);
- mLess.setOnClickListener(this);
+ mMoreOrLess = (ImageButton) findViewById(R.id.edit_more_or_less);
+ mMoreOrLess.setOnClickListener(this);
}
protected EditorListener mListener;
@@ -133,8 +140,7 @@
final View v = mFields.getChildAt(pos);
v.setEnabled(enabled);
}
- mMore.setEnabled(enabled);
- mLess.setEnabled(enabled);
+ mMoreOrLess.setEnabled(enabled);
}
/**
@@ -266,14 +272,13 @@
// When hiding fields, place expandable
if (hidePossible) {
- mMore.setVisibility(mHideOptional ? View.VISIBLE : View.GONE);
- mLess.setVisibility(mHideOptional ? View.GONE : View.VISIBLE);
+ mMoreOrLess.setVisibility(View.VISIBLE);
+ mMoreOrLess.setImageResource(
+ mHideOptional ? R.drawable.ic_btn_round_more : R.drawable.ic_btn_round_less);
} else {
- mMore.setVisibility(View.GONE);
- mLess.setVisibility(View.GONE);
+ mMoreOrLess.setVisibility(View.GONE);
}
- mMore.setEnabled(enabled);
- mLess.setEnabled(enabled);
+ mMoreOrLess.setEnabled(enabled);
}
/**
@@ -354,7 +359,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,32 +381,75 @@
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: {
// Keep around in model, but mark as deleted
mEntry.markDeleted();
- // Remove editor from parent view
- final ViewGroup parent = (ViewGroup)getParent();
- parent.removeView(this);
+// final ViewGroupAnimator animator = ViewGroupAnimator.captureView(getRootView());
+
+// animator.removeView(this);
+ ((ViewGroup) getParent()).removeView(this);
if (mListener != null) {
// Notify listener when present
mListener.onDeleted(this);
}
+
+// animator.animate();
break;
}
- case R.id.edit_more:
- case R.id.edit_less: {
+ case R.id.edit_more_or_less: {
+ // Save focus
+ final View focusedChild = mFields.getFocusedChild();
+ final int focusedViewId = focusedChild == null ? -1 : focusedChild.getId();
+
+ // Snapshot for animation
+// final ViewGroupAnimator animator = ViewGroupAnimator.captureView(getRootView());
+
+ // Reconfigure GUI
mHideOptional = !mHideOptional;
rebuildValues();
+
+ // Restore focus
+ View newFocusView = mFields.findViewById(focusedViewId);
+ if (newFocusView == null || newFocusView.getVisibility() == GONE) {
+ // find first visible child
+ newFocusView = this;
+ }
+ if (newFocusView != null) {
+ newFocusView.requestFocus();
+ }
+
+ // Animate
+// animator.animate();
break;
}
}
}
+ /* 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 +517,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/ui/widget/KindSectionView.java b/src/com/android/contacts/ui/widget/KindSectionView.java
index 221bc16..88f88c7 100644
--- a/src/com/android/contacts/ui/widget/KindSectionView.java
+++ b/src/com/android/contacts/ui/widget/KindSectionView.java
@@ -31,7 +31,6 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.View.OnClickListener;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -41,7 +40,7 @@
* {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a
* section header and a trigger for adding new {@link Data} rows.
*/
-public class KindSectionView extends LinearLayout implements OnClickListener, EditorListener {
+public class KindSectionView extends LinearLayout implements EditorListener {
private static final String TAG = "KindSectionView";
private LayoutInflater mInflater;
@@ -77,7 +76,11 @@
mEditors = (ViewGroup)findViewById(R.id.kind_editors);
mAdd = findViewById(R.id.kind_header);
- mAdd.setOnClickListener(this);
+ mAdd.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ addItem();
+ }
+ });
mAddPlusButton = (ImageView) findViewById(R.id.kind_plus);
@@ -86,8 +89,9 @@
/** {@inheritDoc} */
public void onDeleted(Editor editor) {
- this.updateAddEnabled();
- this.updateEditorsVisible();
+ updateAddEnabled();
+ updateEditorsVisible();
+ updateVisible();
}
/** {@inheritDoc} */
@@ -109,12 +113,17 @@
// Only show the add button if this is a list
mAddPlusButton.setVisibility(mKind.isList ? View.VISIBLE : View.GONE);
- this.rebuildFromState();
- this.updateAddEnabled();
- this.updateEditorsVisible();
+ rebuildFromState();
+ updateAddEnabled();
+ updateEditorsVisible();
+ updateVisible();
}
- public boolean isAnyEditorFilledOut() {
+ public CharSequence getTitle() {
+ return mTitle.getText();
+ }
+
+ public boolean getFieldCount() {
if (mState == null) {
return false;
}
@@ -123,7 +132,7 @@
return false;
}
- int editorCount = mEditors.getChildCount();
+ int editorCount = getEditorCount();
for (int i = 0; i < editorCount; i++) {
GenericEditorView editorView = (GenericEditorView) mEditors.getChildAt(i);
if (editorView.isAnyFieldFilledOut()) {
@@ -144,23 +153,6 @@
// Check if we are displaying anything here
boolean hasEntries = mState.hasMimeEntries(mKind.mimeType);
- if (!mKind.isList) {
- if (hasEntries) {
- // we might have no visible entries. check that, too
- for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
- if (!entry.isVisible()) {
- hasEntries = false;
- break;
- }
- }
- }
-
- if (!hasEntries) {
- EntityModifier.insertChild(mState, mKind);
- hasEntries = true;
- }
- }
-
if (hasEntries) {
int entryIndex = 0;
for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
@@ -170,11 +162,6 @@
final GenericEditorView editor = (GenericEditorView)mInflater.inflate(
R.layout.item_generic_editor, mEditors, false);
editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
- // older versions of android had lists where we now have a single value
- // in these cases we should show the remove button for all but the first value
- // to ensure that nothing is removed
- editor.mDelete.setVisibility((mKind.isList || (entryIndex != 0))
- ? View.VISIBLE : View.GONE);
editor.setEditorListener(this);
mEditors.addView(editor);
entryIndex++;
@@ -183,10 +170,15 @@
}
protected void updateEditorsVisible() {
- final boolean hasChildren = mEditors.getChildCount() > 0;
+ final boolean hasChildren = getEditorCount() > 0;
mEditors.setVisibility(hasChildren ? View.VISIBLE : View.GONE);
}
+ private void updateVisible() {
+ setVisibility(getEditorCount() != 0 ? VISIBLE : GONE);
+ }
+
+
protected void updateAddEnabled() {
// Set enabled state on the "add" view
final boolean canInsert = EntityModifier.canInsert(mState, mKind);
@@ -194,18 +186,18 @@
mAdd.setEnabled(isEnabled);
}
- /** {@inheritDoc} */
- public void onClick(View v) {
- // if this is not a list the plus button is not visible but the user might have clicked
- // the text.
- if (!mKind.isList)
+ public void addItem() {
+ // if this is a list, we can freely add. if not, only allow adding the first
+ if (!mKind.isList && getEditorCount() == 1)
return;
+// final ViewGroupAnimator animator = ViewGroupAnimator.captureView(getRootView());
+
// Insert a new child and rebuild
final ValuesDelta newValues = EntityModifier.insertChild(mState, mKind);
- this.rebuildFromState();
- this.updateAddEnabled();
- this.updateEditorsVisible();
+ rebuildFromState();
+ updateAddEnabled();
+ updateEditorsVisible();
// Find the newly added EditView and set focus.
final int newFieldId = mViewIdGenerator.getId(mState, mKind, newValues, 0);
@@ -213,5 +205,17 @@
if (newField != null) {
newField.requestFocus();
}
+
+ updateVisible();
+
+// animator.animate();
+ }
+
+ public int getEditorCount() {
+ return mEditors.getChildCount();
+ }
+
+ public DataKind getKind() {
+ return mKind;
}
}
diff --git a/src/com/android/contacts/util/AccountSelectionUtil.java b/src/com/android/contacts/util/AccountSelectionUtil.java
index cc46d2b..19e21bc 100644
--- a/src/com/android/contacts/util/AccountSelectionUtil.java
+++ b/src/com/android/contacts/util/AccountSelectionUtil.java
@@ -16,18 +16,13 @@
package com.android.contacts.util;
-import com.android.contacts.ImportVCardActivity;
-import com.android.contacts.R;
-import com.android.contacts.model.ContactsSource;
-import com.android.contacts.model.GoogleSource;
-import com.android.contacts.model.Sources;
-
import android.accounts.Account;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.net.Uri;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@@ -35,7 +30,10 @@
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
-import android.net.Uri;
+
+import com.android.contacts.R;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Sources;
import java.util.List;
@@ -163,10 +161,6 @@
}
public static void doImportFromSim(Context context, Account account) {
- if (account != null) {
- GoogleSource.createMyContactsIfNotExist(account, context);
- }
-
Intent importIntent = new Intent(Intent.ACTION_VIEW);
importIntent.setType("vnd.android.cursor.item/sim-contact");
if (account != null) {
@@ -178,11 +172,8 @@
}
public static void doImportFromSdCard(Context context, Account account) {
- if (account != null) {
- GoogleSource.createMyContactsIfNotExist(account, context);
- }
-
- Intent importIntent = new Intent(context, ImportVCardActivity.class);
+ Intent importIntent = new Intent(context,
+ com.android.contacts.vcard.ImportVCardActivity.class);
if (account != null) {
importIntent.putExtra("account_name", account.name);
importIntent.putExtra("account_type", account.type);
diff --git a/src/com/android/contacts/util/Constants.java b/src/com/android/contacts/util/Constants.java
index e0178ad..433d54d 100644
--- a/src/com/android/contacts/util/Constants.java
+++ b/src/com/android/contacts/util/Constants.java
@@ -19,11 +19,6 @@
import android.app.Service;
import android.provider.ContactsContract.CommonDataKinds.Phone;
-/**
- * Background {@link Service} that is used to keep our process alive long enough
- * for background threads to finish. Started and stopped directly by specific
- * background tasks when needed.
- */
public class Constants {
/**
* Specific MIME-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
diff --git a/src/com/android/contacts/util/DialogManager.java b/src/com/android/contacts/util/DialogManager.java
new file mode 100644
index 0000000..4c6baf3
--- /dev/null
+++ b/src/com/android/contacts/util/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.util;
+
+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/util/LocalizedNameResolver.java b/src/com/android/contacts/util/LocalizedNameResolver.java
new file mode 100644
index 0000000..c2ee90a
--- /dev/null
+++ b/src/com/android/contacts/util/LocalizedNameResolver.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.util;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.content.res.Resources.NotFoundException;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import java.io.IOException;
+
+/**
+ * Retrieves localized names per account type. This allows customizing texts like
+ * "All Contacts" for certain account types, but e.g. "All Friends" or "All Connections" for others.
+ */
+public class LocalizedNameResolver {
+ private static final String TAG = "LocalizedNameResolver";
+
+ /**
+ * Meta-data key for the contacts configuration associated with a sync service.
+ */
+ private static final String METADATA_CONTACTS = "android.provider.CONTACTS_STRUCTURE";
+
+ private static final String CONTACTS_DATA_KIND = "ContactsDataKind";
+
+ /**
+ * Returns the name for All Contacts for the specified account type.
+ */
+ public static String getAllContactsName(Context context, String accountType) {
+ if (context == null) throw new IllegalArgumentException("Context must not be null");
+ if (accountType == null) return null;
+
+ return resolveAllContactsName(context, accountType);
+ }
+
+ /**
+ * Finds "All Contacts"-Name for the specified account type.
+ */
+ private static String resolveAllContactsName(Context context, String accountType) {
+ final AccountManager am = AccountManager.get(context);
+
+ for (AuthenticatorDescription auth : am.getAuthenticatorTypes()) {
+ if (accountType.equals(auth.type)) {
+ return resolveAllContactsNameFromMetaData(context, auth.packageName);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the meta-data XML containing the contacts configuration and
+ * reads the picture priority from that file.
+ */
+ private static String resolveAllContactsNameFromMetaData(Context context, String packageName) {
+ final PackageManager pm = context.getPackageManager();
+ try {
+ PackageInfo pi = pm.getPackageInfo(packageName, PackageManager.GET_SERVICES
+ | PackageManager.GET_META_DATA);
+ if (pi != null && pi.services != null) {
+ for (ServiceInfo si : pi.services) {
+ final XmlResourceParser parser = si.loadXmlMetaData(pm, METADATA_CONTACTS);
+ if (parser != null) {
+ return loadAllContactsNameFromXml(context, parser, packageName);
+ }
+ }
+ }
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Problem loading \"All Contacts\"-name: " + e.toString());
+ }
+ return null;
+ }
+
+ private static String loadAllContactsNameFromXml(Context context, XmlPullParser parser,
+ String packageName) {
+ try {
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Drain comments and whitespace
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("No start tag found");
+ }
+
+ final int depth = parser.getDepth();
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+ String name = parser.getName();
+ if (type == XmlPullParser.START_TAG && CONTACTS_DATA_KIND.equals(name)) {
+ final TypedArray typedArray = context.obtainStyledAttributes(attrs,
+ android.R.styleable.ContactsDataKind);
+ try {
+ // See if a string has been hardcoded directly into the xml
+ final String nonResourceString = typedArray.getNonResourceString(
+ android.R.styleable.ContactsDataKind_allContactsName);
+ if (nonResourceString != null) {
+ return nonResourceString;
+ }
+
+ // See if a resource is referenced. We can't rely on getString
+ // to automatically resolve it as the resource lives in a different package
+ int id = typedArray.getResourceId(
+ android.R.styleable.ContactsDataKind_allContactsName, 0);
+ if (id == 0) return null;
+
+ // Resolve the resource Id
+ final PackageManager packageManager = context.getPackageManager();
+ final Resources resources;
+ try {
+ resources = packageManager.getResourcesForApplication(packageName);
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ try {
+ return resources.getString(id);
+ } catch (NotFoundException e) {
+ return null;
+ }
+ } finally {
+ typedArray.recycle();
+ }
+ }
+ }
+ return null;
+ } catch (XmlPullParserException e) {
+ throw new IllegalStateException("Problem reading XML", e);
+ } catch (IOException e) {
+ throw new IllegalStateException("Problem reading XML", e);
+ }
+ }
+}
diff --git a/src/com/android/contacts/util/ViewGroupAnimator.java b/src/com/android/contacts/util/ViewGroupAnimator.java
new file mode 100644
index 0000000..83e8a7d
--- /dev/null
+++ b/src/com/android/contacts/util/ViewGroupAnimator.java
@@ -0,0 +1,476 @@
+/*
+ * 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.util;
+
+import android.graphics.Rect;
+import android.os.Message;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewRoot;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.Interpolator;
+import android.view.animation.TranslateAnimation;
+import android.view.animation.Animation.AnimationListener;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+/**
+ * Automatically configures natural-feeling animations for views. To use this class,
+ * two calls are required:
+ * <ul>
+ * <li>{@link #captureView(View)} takes a snapshot of all the views and their
+ * positions</li>
+ * <li>{@link #animate()} takes another snapshot, calculates the differences
+ * and creates Translate- and FadeAnimations for the changes</li>
+ * </ul>
+ * To match views, the chain of {@link View#getId()} of each View and all of its parents is
+ * compared. It is therefore not necessary to retain object identity between
+ * {@link #captureView(View)} and {@link #animate()}.
+ * This mechanism works fine for Views that are new or moved. To get a fade out effect for deleted
+ * views, one of two approaches have to be done by the consumer:
+ * <ul>
+ * <li>Instead of actually removing the view, it is only hidden (by setting
+ * its visibility to either {@link View#INVISIBLE} or {@link View#GONE})</li>
+ * <li>{@link #removeView(View)} is used to remove the View. This will hide the view during
+ * the animation and actually remove it from its parent, once the animation is finished.</li>
+ * </ul>
+ * The typical usage pattern looks like this:
+ * <pre>
+ * {@code
+ * final ViewGroupAnimator a = ViewGroupAnimator.captureView(view);
+ * // change view here (except for deletions)
+ * a.removeView(someChildViewThatHasToGo);
+ * a.animate();}
+ * </pre>
+ */
+// TODO: If we don't have any FadeOuts, we could save 150ms by starting the other animations sooner
+// TODO: Create an interface containing the normal functions so that we can mock this for tests
+public class ViewGroupAnimator {
+ /* package */ static final String TAG = "ViewAnimator";
+
+ private static final OnPreDrawListener CANCEL_DRAW_LISTENER = new OnPreDrawListener() {
+ public boolean onPreDraw() {
+ return false;
+ }
+ };
+
+ private static int MOVE_DURATION_MILLIS_DEFAULT = 250;
+ private static int FADE_DURATION_MILLIS_DEFAULT = 300;
+
+ private static int FADE_OUT_OFFSET_MILLIS_DEFAULT = 0;
+ private static int MOVE_OFFSET_MILLIS_DEFAULT = 150;
+ private static int FADE_IN_OFFSET_MILLIS_DEFAULT = 400;
+
+ private static final Interpolator INTERPOLATOR = new AccelerateDecelerateInterpolator();
+
+ private final View mRootView;
+ private final Snapshot mBeforeSnapshot;
+ private final HashSet<View> mViewsToRemove = new HashSet<View>();
+
+ private Runnable mOnAnimationsFinished;
+
+ /**
+ * Cancels all pending animations by calling {@link Animation#cancel()} for
+ * all animations that are currently attached to the provided view or any of its children.
+ */
+ public static void cancelRunningAnimations(View view) {
+ final Animation animation = view.getAnimation();
+ if (animation != null) {
+ animation.cancel();
+ view.setAnimation(null);
+ }
+ if (view instanceof ViewGroup) {
+ final ViewGroup viewGroup = (ViewGroup)view;
+ for (int index = 0; index < viewGroup.getChildCount(); index++) {
+ cancelRunningAnimations(viewGroup.getChildAt(index));
+ }
+ }
+ }
+
+ private ViewGroupAnimator(View rootView) {
+ mRootView = rootView;
+ mBeforeSnapshot = buildSnapshot(rootView);
+ cancelRunningAnimations(rootView);
+ }
+
+ /**
+ * Analyses the given view and its children, builds a snapshot and returns an animator
+ * that can later animate changes. This is the only function to get an instance of this class.
+ */
+ public static final ViewGroupAnimator captureView(View rootView) {
+ return new ViewGroupAnimator(rootView);
+ }
+
+ private void setVisibility(Iterable<View> views, int visibility) {
+ for (View view : views) view.setVisibility(visibility);
+ }
+
+ private void forceInstantRelayout() {
+ // This calls a framework internal function to instantly do the layout
+ // TODO: Find an officially supported way once the framework supports it
+
+ final ViewRoot vr = (ViewRoot) mRootView.getParent();
+ // vr can be null when rapidly chaining animations
+ if (vr != null) vr.handleMessage(Message.obtain(null, ViewRoot.DO_TRAVERSAL));
+ }
+
+ private void enableRedraw() {
+ mRootView.getViewTreeObserver().removeOnPreDrawListener(CANCEL_DRAW_LISTENER);
+ }
+
+ private void disableRedraw() {
+ mRootView.getViewTreeObserver().addOnPreDrawListener(CANCEL_DRAW_LISTENER);
+ }
+
+ /**
+ * Sets a function that should be called once all Animations are finished.
+ */
+ public void setOnAnimationsFinished(Runnable runnable) {
+ mOnAnimationsFinished = runnable;
+ }
+
+ /**
+ * Marks a view for deletion. This view will be set to both {@link View#INVISIBLE} and
+ * {@link View#GONE} during measurement and animation and will be removed from its Parent
+ * (using {@link ViewGroup#removeView(View)}) once all Animations are finished.
+ */
+ public void removeView(View view) {
+ mViewsToRemove.add(view);
+ }
+
+ /**
+ * Performs a difference analysis of positions and visibility, configures animations
+ * and starts them.
+ */
+ public void animate() {
+ disableRedraw();
+ try {
+ setVisibility(mViewsToRemove, View.GONE);
+ forceInstantRelayout();
+ final Snapshot currentSnapshot = buildSnapshot(mRootView);
+ final ArrayList<CachedTranslation> translations = new ArrayList<CachedTranslation>();
+ final HashSet<View> goneViews = new HashSet<View>();
+
+ AnimationManager animationManager = new AnimationManager();
+
+ for (String idChain : currentSnapshot.keySet()) {
+ final ViewInfo afterViewInfo = currentSnapshot.get(idChain);
+
+ if (mViewsToRemove.contains(afterViewInfo.getView())) {
+ // There is special handling for these views below
+ continue;
+ }
+
+ final ViewInfo beforeViewInfo = mBeforeSnapshot.get(idChain);
+
+ final boolean isVisible = afterViewInfo.getVisibility() == View.VISIBLE;
+
+ final boolean existedBefore = beforeViewInfo != null;
+ final boolean wasVisible = existedBefore &&
+ beforeViewInfo.getVisibility() == View.VISIBLE;
+
+ if (isVisible && !wasVisible) {
+ // this is a new View ==> fade it in
+ animationManager.doFade(afterViewInfo.getView(),
+ AnimationManager.FADE_TYPE_NEW);
+ continue;
+ } else if (wasVisible && !isVisible) {
+ if (afterViewInfo.getVisibility() == View.GONE) {
+ goneViews.add(afterViewInfo.getView());
+ } else {
+ animationManager.doFade(afterViewInfo.getView(),
+ AnimationManager.FADE_TYPE_VISIBLE_TO_INVISIBLE);
+ }
+ continue;
+ }
+
+ if (isVisible && wasVisible) {
+ // Check if we have to Transform
+ final Rect afterRectangle = afterViewInfo.getRectangle();
+ final Rect beforeRectangle = beforeViewInfo.getRectangle();
+
+ final int diffX = afterRectangle.left - beforeRectangle.left;
+ final int diffY = afterRectangle.top - beforeRectangle.top;
+
+ final boolean doTranslate = diffX != 0 || diffY != 0;
+
+ if (doTranslate) {
+ translations.add(new CachedTranslation(afterViewInfo.getView(),
+ afterRectangle, diffX, diffY));
+ continue;
+ }
+ }
+ }
+
+ // Set views to Invisible, because we need their space for the layout
+ setVisibility(mViewsToRemove, View.INVISIBLE);
+ setVisibility(goneViews, View.INVISIBLE);
+ forceInstantRelayout();
+
+ for (CachedTranslation translation : translations) {
+ final Rect intermediatePosition = translation.getIntermediatePosition();
+ final int addX = intermediatePosition.left - translation.getView().getLeft();
+ final int addY = intermediatePosition.top - translation.getView().getTop();
+ animationManager.doTranslation(translation.getView(),
+ addX - translation.getDiffX(), addX,
+ addY - translation.getDiffY(), addY);
+ }
+
+ for (final View view : mViewsToRemove) {
+ animationManager.doFade(view, AnimationManager.FADE_TYPE_VISIBLE_TO_REMOVED);
+ }
+
+ for (final View view : goneViews) {
+ animationManager.doFade(view, AnimationManager.FADE_TYPE_VISIBLE_TO_GONE);
+ }
+ } finally {
+ enableRedraw();
+ }
+ }
+
+ private static Snapshot buildSnapshot(View rootView) {
+ final Snapshot result = new Snapshot();
+ buildSnapshotRecursive(rootView, result, "");
+ return result;
+ }
+
+ private static void buildSnapshotRecursive(View parentView,
+ Snapshot targetSnapshot, final String parentIdChain) {
+ if (!(parentView instanceof ViewGroup)) return;
+
+ final ViewGroup parentViewGroup = (ViewGroup) parentView;
+ for (int index = 0; index < parentViewGroup.getChildCount(); index++) {
+ final View view = parentViewGroup.getChildAt(index);
+ final int id = view.getId();
+ final String idChain;
+ if (id != View.NO_ID) {
+ idChain = parentIdChain + "/" + id;
+ } else {
+ idChain = parentIdChain + "/i" + index;
+ }
+
+ targetSnapshot.put(idChain, new ViewInfo(view));
+
+ buildSnapshotRecursive(view, targetSnapshot, idChain);
+ }
+ }
+
+ private final class AnimationManager implements AnimationListener {
+ private int mCountCalled = 0;
+
+ private static final int FADE_TYPE_VISIBLE_TO_GONE = 1;
+ private static final int FADE_TYPE_VISIBLE_TO_INVISIBLE = 2;
+ private static final int FADE_TYPE_VISIBLE_TO_REMOVED = 3;
+ private static final int FADE_TYPE_NEW = 4;
+
+ private static final int CLEANUP_NO_ACTION = 0;
+ private static final int CLEANUP_CLEAR_ANIMATION = 1;
+ private static final int CLEANUP_REMOVE = 2;
+ private static final int CLEANUP_SET_TO_GONE = 3;
+
+ private final HashMap<View, AnimationInfo> mAnimations = new HashMap<View, AnimationInfo>();
+
+ public void doTranslation(View view, int fromX, int toX, int fromY, int toY) {
+ final TranslateAnimation animation = new TranslateAnimation(
+ fromX,
+ toX,
+ fromY,
+ toY);
+ animation.setFillBefore(true);
+ animation.setFillAfter(true);
+ animation.setFillEnabled(true);
+ animation.setDuration(MOVE_DURATION_MILLIS_DEFAULT);
+ animation.setStartOffset(MOVE_OFFSET_MILLIS_DEFAULT);
+ animation.setInterpolator(INTERPOLATOR);
+ animation.setAnimationListener(this);
+
+ view.startAnimation(animation);
+
+ mAnimations.put(view, new AnimationInfo(animation, CLEANUP_CLEAR_ANIMATION));
+ }
+
+ public void doFade(View view, int fadeType) {
+ final float fromAlpha = fadeType == FADE_TYPE_NEW ? 0.0f : 1.0f;
+ final float toAlpha = fadeType == FADE_TYPE_NEW ? 1.0f : 0.0f;
+ final AlphaAnimation animation = new AlphaAnimation(fromAlpha, toAlpha);
+ animation.setDuration(FADE_DURATION_MILLIS_DEFAULT);
+ animation.setInterpolator(INTERPOLATOR);
+ animation.setAnimationListener(this);
+
+ final int cleanUpAction;
+ final boolean fill;
+ switch (fadeType) {
+ case FADE_TYPE_NEW:
+ // No clean up necessary
+ cleanUpAction = CLEANUP_NO_ACTION;
+ fill = false;
+ animation.setStartOffset(FADE_IN_OFFSET_MILLIS_DEFAULT);
+ break;
+ case FADE_TYPE_VISIBLE_TO_GONE:
+ cleanUpAction = CLEANUP_SET_TO_GONE;
+ fill = true;
+ animation.setStartOffset(FADE_OUT_OFFSET_MILLIS_DEFAULT);
+ break;
+ case FADE_TYPE_VISIBLE_TO_INVISIBLE:
+ // No clean up necessary
+ cleanUpAction = CLEANUP_NO_ACTION;
+ fill = false;
+ animation.setStartOffset(FADE_OUT_OFFSET_MILLIS_DEFAULT);
+ break;
+ case FADE_TYPE_VISIBLE_TO_REMOVED:
+ cleanUpAction = CLEANUP_REMOVE;
+ fill = true;
+ animation.setStartOffset(FADE_OUT_OFFSET_MILLIS_DEFAULT);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown fadeType");
+ }
+ if (fill) {
+ animation.setFillBefore(true);
+ animation.setFillAfter(true);
+ animation.setFillEnabled(true);
+ }
+ mAnimations.put(view, new AnimationInfo(animation, cleanUpAction));
+
+ view.startAnimation(animation);
+ }
+
+ public void onAnimationEnd(Animation animation) {
+ mCountCalled++;
+ if (mCountCalled == mAnimations.size()) {
+ Log.d(TAG, "Cleaning up animations");
+
+ cleanUp();
+
+ if (mOnAnimationsFinished != null) mOnAnimationsFinished.run();
+ }
+ }
+
+ private void cleanUp() {
+ for (final View view : mAnimations.keySet()) {
+ final AnimationInfo animationInfo = mAnimations.get(view);
+ switch (animationInfo.getCleanUpAction()) {
+ case CLEANUP_NO_ACTION:
+ case CLEANUP_CLEAR_ANIMATION:
+ if (view.getAnimation() != animationInfo.getAnimation()) continue;
+ view.clearAnimation();
+ break;
+ case CLEANUP_REMOVE:
+ final ViewGroup parentGroup = (ViewGroup) view.getParent();
+ // has this view already been removed before?
+ if (parentGroup != null) parentGroup.removeView(view);
+ break;
+ case CLEANUP_SET_TO_GONE:
+ if (view.getAnimation() != animationInfo.getAnimation()) continue;
+ view.clearAnimation();
+ view.setVisibility(View.GONE);
+ break;
+ default:
+ throw new IllegalStateException("Unknown cleanup type");
+ }
+ }
+ }
+
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ public void onAnimationStart(Animation animation) {
+ }
+ }
+
+ private final static class AnimationInfo {
+ private final Animation mAnimation;
+ private final int mCleanUpAction;
+
+ public Animation getAnimation() {
+ return mAnimation;
+ }
+ public int getCleanUpAction() {
+ return mCleanUpAction;
+ }
+
+ public AnimationInfo(Animation animation, int cleanUpAction) {
+ mAnimation = animation;
+ mCleanUpAction = cleanUpAction;
+ }
+ }
+
+ private final static class ViewInfo {
+ private final View mView;
+ private final Rect mRectangle;
+ private final int mVisibility;
+
+ public Rect getRectangle() {
+ return mRectangle;
+ }
+ public View getView() {
+ return mView;
+ }
+ public int getVisibility() {
+ return mVisibility;
+ }
+
+ public ViewInfo(View view) {
+ mView = view;
+ mRectangle = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
+ mVisibility = view.getVisibility();
+ }
+ }
+
+ /**
+ * Shortcut to HashMap<String, ViewInfo>
+ */
+ private final static class Snapshot extends HashMap<String, ViewInfo> {
+
+ }
+
+ private final static class CachedTranslation {
+ private final View mView;
+ private final Rect mIntermediatePosition;
+ private final int mDiffX;
+ private final int mDiffY;
+
+ public View getView() {
+ return mView;
+ }
+
+ public Rect getIntermediatePosition() {
+ return mIntermediatePosition;
+ }
+
+ public int getDiffX() {
+ return mDiffX;
+ }
+
+ public int getDiffY() {
+ return mDiffY;
+ }
+
+ public CachedTranslation(View view, Rect intermediatePosition, int diffX, int diffY) {
+ mView = view;
+ mIntermediatePosition = intermediatePosition;
+ mDiffX = diffX;
+ mDiffY = diffY;
+ }
+ }
+}
diff --git a/src/com/android/contacts/vcard/ExportProcessor.java b/src/com/android/contacts/vcard/ExportProcessor.java
new file mode 100644
index 0000000..7e7c7c8
--- /dev/null
+++ b/src/com/android/contacts/vcard/ExportProcessor.java
@@ -0,0 +1,256 @@
+/*
+ * 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.vcard;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import com.android.contacts.ContactsListActivity;
+import com.android.contacts.R;
+import com.android.vcard.VCardComposer;
+import com.android.vcard.VCardConfig;
+
+import java.io.FileNotFoundException;
+import java.io.OutputStream;
+import java.util.LinkedList;
+import java.util.Queue;
+
+public class ExportProcessor {
+ private static final String LOG_TAG = "ExportProcessor";
+
+ private final Service mService;
+
+ private ContentResolver mResolver;
+ private NotificationManager mNotificationManager;
+
+ boolean mCanceled;
+
+ boolean mReadyForRequest;
+ private final Queue<ExportRequest> mPendingRequests =
+ new LinkedList<ExportRequest>();
+
+ private RemoteViews mProgressViews;
+
+ public ExportProcessor(Service service) {
+ mService = service;
+ }
+
+ /* package */ ThreadStarter mThreadStarter = new ThreadStarter() {
+ public void start() {
+ final Thread thread = new Thread(new Runnable() {
+ public void run() {
+ process();
+ }
+ });
+ thread.start();
+ }
+ };
+
+ public synchronized void pushRequest(ExportRequest parameter) {
+ if (mResolver == null) {
+ // Service object may not ready at the construction time
+ // (e.g. ContentResolver may be null).
+ mResolver = mService.getContentResolver();
+ mNotificationManager =
+ (NotificationManager)mService.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ final boolean needThreadStart;
+ if (!mReadyForRequest) {
+ needThreadStart = true;
+ } else {
+ needThreadStart = false;
+ }
+ mPendingRequests.add(parameter);
+ if (needThreadStart) {
+ mThreadStarter.start();
+ }
+
+ mReadyForRequest = true;
+ }
+
+ /* package */ void process() {
+ if (!mReadyForRequest) {
+ throw new RuntimeException(
+ "process() is called before request being pushed "
+ + "or after this object's finishing its processing.");
+ }
+
+ try {
+ while (!mCanceled) {
+ final ExportRequest parameter;
+ synchronized (this) {
+ if (mPendingRequests.size() == 0) {
+ mReadyForRequest = false;
+ break;
+ } else {
+ parameter = mPendingRequests.poll();
+ }
+ } // synchronized (this)
+ handleOneRequest(parameter);
+ }
+
+ doFinishNotification(mService.getString(R.string.exporting_vcard_finished_title),
+ "");
+ } finally {
+ // Not thread safe. Just in case.
+ // TODO: verify this works fine.
+ mReadyForRequest = false;
+ }
+ }
+
+ /* package */ void handleOneRequest(ExportRequest request) {
+ boolean shouldCallFinish = true;
+ VCardComposer composer = null;
+ try {
+ final Uri uri = request.destUri;
+ final OutputStream outputStream;
+ try {
+ outputStream = mResolver.openOutputStream(uri);
+ } catch (FileNotFoundException e) {
+ // Need concise title.
+
+ final String errorReason =
+ mService.getString(R.string.fail_reason_could_not_open_file,
+ uri, e.getMessage());
+ shouldCallFinish = false;
+ doFinishNotification(errorReason, "");
+ return;
+ }
+ final String exportType = request.exportType;
+ final int vcardType;
+ if (TextUtils.isEmpty(exportType)) {
+ vcardType = VCardConfig.getVCardTypeFromString(
+ mService.getString(R.string.config_export_vcard_type));
+ } else {
+ vcardType = VCardConfig.getVCardTypeFromString(exportType);
+ }
+
+ composer = new VCardComposer(mService, vcardType, true);
+
+ // for test
+ // int vcardType = (VCardConfig.VCARD_TYPE_V21_GENERIC |
+ // VCardConfig.FLAG_USE_QP_TO_PRIMARY_PROPERTIES);
+ // composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);
+
+ composer.addHandler(composer.new HandlerForOutputStream(outputStream));
+
+ if (!composer.init()) {
+ final String errorReason = composer.getErrorReason();
+ Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
+ final String translatedErrorReason =
+ translateComposerError(errorReason);
+ final String title =
+ mService.getString(R.string.fail_reason_could_not_initialize_exporter,
+ translatedErrorReason);
+ doFinishNotification(title, "");
+ return;
+ }
+
+ final int total = composer.getCount();
+ if (total == 0) {
+ final String title =
+ mService.getString(R.string.fail_reason_no_exportable_contact);
+ doFinishNotification(title, "");
+ return;
+ }
+
+ int current = 1; // 1-origin
+ while (!composer.isAfterLast()) {
+ if (mCanceled) {
+ return;
+ }
+ if (!composer.createOneEntry()) {
+ final String errorReason = composer.getErrorReason();
+ Log.e(LOG_TAG, "Failed to read a contact: " + errorReason);
+ final String translatedErrorReason =
+ translateComposerError(errorReason);
+ final String title =
+ mService.getString(R.string.fail_reason_error_occurred_during_export,
+ translatedErrorReason);
+ doFinishNotification(title, "");
+ return;
+ }
+ doProgressNotification(uri, total, current);
+ current++;
+ }
+ } finally {
+ if (composer != null) {
+ composer.terminate();
+ }
+ }
+ }
+
+ private String translateComposerError(String errorMessage) {
+ final Resources resources = mService.getResources();
+ if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
+ return resources.getString(R.string.composer_failed_to_get_database_infomation);
+ } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
+ return resources.getString(R.string.composer_has_no_exportable_contact);
+ } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) {
+ return resources.getString(R.string.composer_not_initialized);
+ } else {
+ return errorMessage;
+ }
+ }
+
+ private void doProgressNotification(Uri uri, int total, int current) {
+ final String title = mService.getString(R.string.exporting_contact_list_title);
+ final String message =
+ mService.getString(R.string.exporting_contact_list_message, uri);
+
+ final RemoteViews remoteViews = new RemoteViews(mService.getPackageName(),
+ R.layout.status_bar_ongoing_event_progress_bar);
+ remoteViews.setTextViewText(R.id.description, message);
+ remoteViews.setProgressBar(R.id.progress_bar, total, current, (total == -1));
+
+ final String percentage = mService.getString(R.string.percentage,
+ String.valueOf((current * 100)/total));
+ remoteViews.setTextViewText(R.id.progress_text, percentage);
+ remoteViews.setImageViewResource(R.id.appIcon, android.R.drawable.stat_sys_upload);
+
+ final Notification notification = new Notification();
+ notification.icon = android.R.drawable.stat_sys_upload;
+ notification.flags |= Notification.FLAG_ONGOING_EVENT;
+ notification.tickerText = title;
+ notification.contentView = remoteViews;
+ notification.contentIntent =
+ PendingIntent.getActivity(mService, 0,
+ new Intent(mService, ContactsListActivity.class), 0);
+ mNotificationManager.notify(VCardService.EXPORT_NOTIFICATION_ID, notification);
+ }
+
+ private void doFinishNotification(final String title, final String message) {
+ final Notification notification = new Notification();
+ notification.icon = android.R.drawable.stat_sys_upload_done;
+ notification.flags |= Notification.FLAG_AUTO_CANCEL;
+ notification.setLatestEventInfo(mService, title, message, null);
+ final Intent intent = new Intent(mService, ContactsListActivity.class);
+ notification.contentIntent =
+ PendingIntent.getActivity(mService, 0, intent, 0);
+ mNotificationManager.notify(VCardService.EXPORT_NOTIFICATION_ID, notification);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/ExportRequest.java b/src/com/android/contacts/vcard/ExportRequest.java
new file mode 100644
index 0000000..fae2d07
--- /dev/null
+++ b/src/com/android/contacts/vcard/ExportRequest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.vcard;
+
+import android.net.Uri;
+
+public class ExportRequest {
+ public final Uri destUri;
+ /**
+ * Can be null.
+ */
+ public final String exportType;
+
+ public ExportRequest(Uri destUri) {
+ this(destUri, null);
+ }
+
+ public ExportRequest(Uri destUri, String exportType) {
+ this.destUri = destUri;
+ this.exportType = exportType;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ExportVCardActivity.java b/src/com/android/contacts/vcard/ExportVCardActivity.java
similarity index 65%
rename from src/com/android/contacts/ExportVCardActivity.java
rename to src/com/android/contacts/vcard/ExportVCardActivity.java
index 5bccc7a..43ae858 100644
--- a/src/com/android/contacts/ExportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ExportVCardActivity.java
@@ -13,27 +13,41 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.contacts;
+package com.android.contacts.vcard;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
import android.content.res.Resources;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
import android.os.PowerManager;
-import android.pim.vcard.VCardComposer;
+import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
+import com.android.contacts.R;
+import com.android.vcard.VCardComposer;
+import com.android.vcard.VCardConfig;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Queue;
import java.util.Set;
/**
@@ -55,21 +69,76 @@
private int mFileIndexMinimum;
private int mFileIndexMaximum;
private String mFileNameExtension;
- private String mVCardTypeStr;
private Set<String> mExtensionsToConsider;
- private ProgressDialog mProgressDialog;
- private String mExportingFileName;
-
- private Handler mHandler = new Handler();
-
- // Used temporaly when asking users to confirm the file name
+ // Used temporarily when asking users to confirm the file name
private String mTargetFileName;
- // String for storing error reason temporaly.
+ // String for storing error reason temporarily.
private String mErrorReason;
- private ActualExportThread mActualExportThread;
+
+ private class CustomConnection implements ServiceConnection {
+ private Messenger mMessenger;
+ private Queue<ExportRequest> mPendingRequests = new LinkedList<ExportRequest>();
+
+ public void doBindService() {
+ bindService(new Intent(ExportVCardActivity.this,
+ VCardService.class), this, Context.BIND_AUTO_CREATE);
+ }
+
+ public synchronized void requestSend(final ExportRequest parameter) {
+ if (mMessenger != null) {
+ sendMessage(parameter);
+ } else {
+ mPendingRequests.add(parameter);
+ }
+ }
+
+ private void sendMessage(final ExportRequest request) {
+ try {
+ mMessenger.send(Message.obtain(null,
+ VCardService.MSG_EXPORT_REQUEST,
+ request));
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
+ runOnUiThread(new ErrorReasonDisplayer(
+ getString(R.string.fail_reason_unknown)));
+ }
+ }
+
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ synchronized (this) {
+ mMessenger = new Messenger(service);
+ // Send pending requests thrown from this Activity before an actual connection
+ // is established.
+ while (!mPendingRequests.isEmpty()) {
+ final ExportRequest parameter = mPendingRequests.poll();
+ if (parameter == null) {
+ throw new NullPointerException();
+ }
+ sendMessage(parameter);
+ }
+
+ unbindService(this);
+ finish();
+ }
+ }
+
+ public void onServiceDisconnected(ComponentName name) {
+ synchronized (this) {
+ if (!mPendingRequests.isEmpty()) {
+ Log.w(LOG_TAG, "Some request(s) are dropped.");
+ }
+ // Set to null so that we can detect inappropriate re-connection toward
+ // the Service via NullPointerException;
+ mPendingRequests = null;
+ mMessenger = null;
+ }
+ }
+ }
+
+ private final CustomConnection mConnection = new CustomConnection();
private class CancelListener
implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
@@ -101,129 +170,28 @@
}
private class ExportConfirmationListener implements DialogInterface.OnClickListener {
- private final String mFileName;
+ private final Uri mDestUri;
public ExportConfirmationListener(String fileName) {
- mFileName = fileName;
+ this(Uri.parse("file://" + fileName));
+ }
+
+ public ExportConfirmationListener(Uri uri) {
+ mDestUri = uri;
}
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
- mActualExportThread = new ActualExportThread(mFileName);
- showDialog(R.id.dialog_exporting_vcard);
+ mConnection.doBindService();
+
+ final ExportRequest request = new ExportRequest(mDestUri);
+
+ // The connection object will call finish().
+ mConnection.requestSend(request);
}
}
}
- private class ActualExportThread extends Thread
- implements DialogInterface.OnCancelListener {
- private PowerManager.WakeLock mWakeLock;
- private boolean mCanceled = false;
-
- public ActualExportThread(String fileName) {
- mExportingFileName = fileName;
- PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
- mWakeLock = powerManager.newWakeLock(
- PowerManager.SCREEN_DIM_WAKE_LOCK |
- PowerManager.ON_AFTER_RELEASE, LOG_TAG);
- }
-
- @Override
- public void run() {
- boolean shouldCallFinish = true;
- mWakeLock.acquire();
- VCardComposer composer = null;
- try {
- OutputStream outputStream = null;
- try {
- outputStream = new FileOutputStream(mExportingFileName);
- } catch (FileNotFoundException e) {
- final String errorReason =
- getString(R.string.fail_reason_could_not_open_file,
- mExportingFileName, e.getMessage());
- shouldCallFinish = false;
- mHandler.post(new ErrorReasonDisplayer(errorReason));
- return;
- }
-
- composer = new VCardComposer(ExportVCardActivity.this, mVCardTypeStr, true);
- /*int vcardType = (VCardConfig.VCARD_TYPE_V21_GENERIC |
- VCardConfig.FLAG_USE_QP_TO_PRIMARY_PROPERTIES);
- composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);*/
-
- composer.addHandler(composer.new HandlerForOutputStream(outputStream));
-
- if (!composer.init()) {
- final String errorReason = composer.getErrorReason();
- Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
- final String translatedErrorReason =
- translateComposerError(errorReason);
- mHandler.post(new ErrorReasonDisplayer(
- getString(R.string.fail_reason_could_not_initialize_exporter,
- translatedErrorReason)));
- shouldCallFinish = false;
- return;
- }
-
- int size = composer.getCount();
-
- if (size == 0) {
- mHandler.post(new ErrorReasonDisplayer(
- getString(R.string.fail_reason_no_exportable_contact)));
- shouldCallFinish = false;
- return;
- }
-
- mProgressDialog.setProgressNumberFormat(
- getString(R.string.exporting_contact_list_progress));
- mProgressDialog.setMax(size);
- mProgressDialog.setProgress(0);
-
- while (!composer.isAfterLast()) {
- if (mCanceled) {
- return;
- }
- if (!composer.createOneEntry()) {
- final String errorReason = composer.getErrorReason();
- Log.e(LOG_TAG, "Failed to read a contact: " + errorReason);
- final String translatedErrorReason =
- translateComposerError(errorReason);
- mHandler.post(new ErrorReasonDisplayer(
- getString(R.string.fail_reason_error_occurred_during_export,
- translatedErrorReason)));
- shouldCallFinish = false;
- return;
- }
- mProgressDialog.incrementProgressBy(1);
- }
- } finally {
- if (composer != null) {
- composer.terminate();
- }
- mWakeLock.release();
- mProgressDialog.dismiss();
- if (shouldCallFinish && !isFinishing()) {
- finish();
- }
- }
- }
-
- @Override
- public void finalize() {
- if (mWakeLock != null && mWakeLock.isHeld()) {
- mWakeLock.release();
- }
- }
-
- public void cancel() {
- mCanceled = true;
- }
-
- public void onCancel(DialogInterface dialog) {
- cancel();
- }
- }
-
private String translateComposerError(String errorMessage) {
Resources resources = getResources();
if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
@@ -245,7 +213,6 @@
mFileNamePrefix = getString(R.string.config_export_file_prefix);
mFileNameSuffix = getString(R.string.config_export_file_suffix);
mFileNameExtension = getString(R.string.config_export_file_extension);
- mVCardTypeStr = getString(R.string.config_export_vcard_type);
mExtensionsToConsider = new HashSet<String>();
mExtensionsToConsider.add(mFileNameExtension);
@@ -269,7 +236,7 @@
}
@Override
- protected Dialog onCreateDialog(int id) {
+ protected Dialog onCreateDialog(int id, Bundle bundle) {
switch (id) {
case R.id.dialog_export_confirmation: {
return getExportConfirmationDialog();
@@ -293,44 +260,25 @@
.setPositiveButton(android.R.string.ok, mCancelListener);
return builder.create();
}
- case R.id.dialog_exporting_vcard: {
- if (mProgressDialog == null) {
- String title = getString(R.string.exporting_contact_list_title);
- String message = getString(R.string.exporting_contact_list_message,
- mExportingFileName);
- mProgressDialog = new ProgressDialog(ExportVCardActivity.this);
- mProgressDialog.setTitle(title);
- mProgressDialog.setMessage(message);
- mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
- mProgressDialog.setOnCancelListener(mActualExportThread);
- mActualExportThread.start();
- }
- return mProgressDialog;
- }
}
- return super.onCreateDialog(id);
+ return super.onCreateDialog(id, bundle);
}
@Override
- protected void onPrepareDialog(int id, Dialog dialog) {
+ protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
if (id == R.id.dialog_fail_to_export_with_reason) {
((AlertDialog)dialog).setMessage(getErrorReason());
} else if (id == R.id.dialog_export_confirmation) {
((AlertDialog)dialog).setMessage(
getString(R.string.confirm_export_message, mTargetFileName));
} else {
- super.onPrepareDialog(id, dialog);
+ super.onPrepareDialog(id, dialog, args);
}
}
@Override
protected void onStop() {
super.onStop();
- if (mActualExportThread != null) {
- // The Activity is no longer visible. Stop the thread.
- mActualExportThread.cancel();
- mActualExportThread = null;
- }
if (!isFinishing()) {
finish();
@@ -445,13 +393,6 @@
.create();
}
- public void cancelExport() {
- if (mActualExportThread != null) {
- mActualExportThread.cancel();
- mActualExportThread = null;
- }
- }
-
public String getErrorReason() {
return mErrorReason;
}
diff --git a/src/com/android/contacts/vcard/ImportProcessor.java b/src/com/android/contacts/vcard/ImportProcessor.java
new file mode 100644
index 0000000..6f765f9
--- /dev/null
+++ b/src/com/android/contacts/vcard/ImportProcessor.java
@@ -0,0 +1,370 @@
+/*
+ * 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.vcard;
+
+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.Intent;
+import android.net.Uri;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+import com.android.contacts.R;
+import com.android.vcard.VCardEntryCommitter;
+import com.android.vcard.VCardEntryConstructor;
+import com.android.vcard.VCardInterpreter;
+import com.android.vcard.VCardParser;
+import com.android.vcard.VCardParser_V21;
+import com.android.vcard.VCardParser_V30;
+import com.android.vcard.exception.VCardException;
+import com.android.vcard.exception.VCardNestedException;
+import com.android.vcard.exception.VCardNotSupportedException;
+import com.android.vcard.exception.VCardVersionException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+/**
+ * Class for processing incoming import request from {@link ImportVCardActivity}.
+ *
+ * This class is designed so that a user ({@link Service}) does not need to (and should not)
+ * recreate multiple instances, as this holds total count of vCard entries to be imported.
+ */
+public class ImportProcessor {
+ private static final String LOG_TAG = "ImportRequestProcessor";
+
+ private final Context mContext;
+
+ private ContentResolver mResolver;
+ private NotificationManager mNotificationManager;
+
+ private final List<Uri> mFailedUris = new ArrayList<Uri>();
+ private final List<Uri> mCreatedUris = new ArrayList<Uri>();
+ private final ImportProgressNotifier mNotifier = new ImportProgressNotifier();
+
+ private VCardParser mVCardParser;
+
+ /**
+ * Meaning a controller of this object requests the operation should be canceled
+ * or not, which implies {@link #mReadyForRequest} should be set to false soon, but
+ * it does not meaning cancel request is able to immediately stop this object,
+ * so we have two variables.
+ */
+ private boolean mCanceled;
+
+ /**
+ * Meaning that this object is able to accept import requests.
+ */
+ private boolean mReadyForRequest;
+ private final Queue<ImportRequest> mPendingRequests =
+ new LinkedList<ImportRequest>();
+
+ // For testability.
+ /* package */ ThreadStarter mThreadStarter = new ThreadStarter() {
+ public void start() {
+ final Thread thread = new Thread(new Runnable() {
+ public void run() {
+ process();
+ }
+ });
+ thread.start();
+ }
+ };
+ /* package */ interface CommitterGenerator {
+ public VCardEntryCommitter generate(ContentResolver resolver);
+ }
+ /* package */ class DefaultCommitterGenerator implements CommitterGenerator {
+ public VCardEntryCommitter generate(ContentResolver resolver) {
+ return new VCardEntryCommitter(mResolver);
+ }
+ }
+ /* package */ CommitterGenerator mCommitterGenerator = new DefaultCommitterGenerator();
+
+ public ImportProcessor(final Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Checks this object and initialize it if not.
+ *
+ * This method is needed since {@link VCardService} is not ready when this object is
+ * created and we need to delay this initialization, while we want to initialize
+ * this object soon in tests.
+ */
+ /* package */ void ensureInit() {
+ if (mResolver == null) {
+ // Service object may not ready at the construction time
+ // (e.g. ContentResolver may be null).
+ mResolver = mContext.getContentResolver();
+ mNotificationManager =
+ (NotificationManager)mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+ }
+
+ public synchronized void pushRequest(ImportRequest parameter) {
+ ensureInit();
+
+ final boolean needThreadStart;
+ if (!mReadyForRequest) {
+ mFailedUris.clear();
+ mCreatedUris.clear();
+
+ mNotifier.init(mContext, mNotificationManager);
+ needThreadStart = true;
+ } else {
+ needThreadStart = false;
+ }
+ final int count = parameter.entryCount;
+ if (count > 0) {
+ mNotifier.addTotalCount(count);
+ }
+ mPendingRequests.add(parameter);
+ if (needThreadStart) {
+ mThreadStarter.start();
+ }
+
+ mReadyForRequest = true;
+ }
+
+ /**
+ * Starts processing import requests. Never stops until all given requests are
+ * processed or some error happens, assuming this method is called from a
+ * {@link Thread} object.
+ */
+ /* package */ void process() {
+ if (!mReadyForRequest) {
+ throw new RuntimeException(
+ "process() is called before request being pushed "
+ + "or after this object's finishing its processing.");
+ }
+ try {
+ while (!mCanceled) {
+ final ImportRequest parameter;
+ synchronized (this) {
+ if (mPendingRequests.size() == 0) {
+ mReadyForRequest = false;
+ break;
+ } else {
+ parameter = mPendingRequests.poll();
+ }
+ } // synchronized (this)
+ handleOneRequest(parameter);
+ }
+
+ // Currenty we don't have an appropriate way to let users see all entries
+ // imported in this procedure. Instead, we show them entries only when
+ // there's just one created uri.
+ doFinishNotification(mCreatedUris.size() > 0 ? mCreatedUris.get(0) : null);
+ } finally {
+ // TODO: verify this works fine.
+ mReadyForRequest = false; // Just in case.
+ mNotifier.resetTotalCount();
+ }
+ }
+
+ /**
+ * Would be run inside synchronized block.
+ */
+ /* package */ boolean handleOneRequest(final ImportRequest parameter) {
+ if (mCanceled) {
+ Log.i(LOG_TAG, "Canceled before actually handling parameter ("
+ + parameter.uri + ")");
+ return false;
+ }
+ final int[] possibleVCardVersions;
+ if (parameter.vcardVersion == ImportVCardActivity.VCARD_VERSION_AUTO_DETECT) {
+ /**
+ * Note: this code assumes that a given Uri is able to be opened more than once,
+ * which may not be true in certain conditions.
+ */
+ possibleVCardVersions = new int[] {
+ ImportVCardActivity.VCARD_VERSION_V21,
+ ImportVCardActivity.VCARD_VERSION_V30
+ };
+ } else {
+ possibleVCardVersions = new int[] {
+ parameter.vcardVersion
+ };
+ }
+
+ final Uri uri = parameter.uri;
+ final Account account = parameter.account;
+ final int estimatedVCardType = parameter.estimatedVCardType;
+ final String estimatedCharset = parameter.estimatedCharset;
+
+ final VCardEntryConstructor constructor =
+ new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset);
+ final VCardEntryCommitter committer = mCommitterGenerator.generate(mResolver);
+ constructor.addEntryHandler(committer);
+ constructor.addEntryHandler(mNotifier);
+
+ final boolean successful =
+ readOneVCard(uri, estimatedVCardType, estimatedCharset,
+ constructor, possibleVCardVersions);
+ if (successful) {
+ List<Uri> uris = committer.getCreatedUris();
+ if (uris != null) {
+ mCreatedUris.addAll(uris);
+ } else {
+ // Not critical, but suspicious.
+ Log.w(LOG_TAG,
+ "Created Uris is null while the creation itself is successful.");
+ }
+ } else {
+ mFailedUris.add(uri);
+ }
+
+ return successful;
+ }
+
+ /*
+ private void doErrorNotification(int id) {
+ final Notification notification = new Notification();
+ notification.icon = android.R.drawable.stat_sys_download_done;
+ notification.flags |= Notification.FLAG_AUTO_CANCEL;
+ final String title = mService.getString(R.string.reading_vcard_failed_title);
+ final PendingIntent intent =
+ PendingIntent.getActivity(mService, 0, new Intent(), 0);
+ notification.setLatestEventInfo(mService, title, "", intent);
+ mNotificationManager.notify(MESSAGE_ID, notification);
+ }
+ */
+
+ private void doFinishNotification(final Uri createdUri) {
+ final Notification notification = new Notification();
+ notification.icon = android.R.drawable.stat_sys_download_done;
+ notification.flags |= Notification.FLAG_AUTO_CANCEL;
+
+ final String title = mContext.getString(R.string.importing_vcard_finished_title);
+
+ final Intent intent;
+ if (createdUri != null) {
+ final long rawContactId = ContentUris.parseId(createdUri);
+ final Uri contactUri = RawContacts.getContactLookupUri(
+ mResolver, ContentUris.withAppendedId(
+ RawContacts.CONTENT_URI, rawContactId));
+ intent = new Intent(Intent.ACTION_VIEW, contactUri);
+ } else {
+ intent = null;
+ }
+
+ notification.setLatestEventInfo(mContext, title, "",
+ PendingIntent.getActivity(mContext, 0, intent, 0));
+ mNotificationManager.notify(VCardService.IMPORT_NOTIFICATION_ID, notification);
+ }
+
+ // Make package private for testing use.
+ /* package */ boolean readOneVCard(Uri uri, int vcardType, String charset,
+ final VCardInterpreter interpreter,
+ final int[] possibleVCardVersions) {
+ boolean successful = false;
+ final int length = possibleVCardVersions.length;
+ for (int i = 0; i < length; i++) {
+ InputStream is = null;
+ final int vcardVersion = possibleVCardVersions[i];
+ try {
+ if (i > 0 && (interpreter instanceof VCardEntryConstructor)) {
+ // Let the object clean up internal temporary objects,
+ ((VCardEntryConstructor) interpreter).clear();
+ }
+
+ is = mResolver.openInputStream(uri);
+
+ // We need synchronized block here,
+ // since we need to handle mCanceled and mVCardParser at once.
+ // In the worst case, a user may call cancel() just before creating
+ // mVCardParser.
+ synchronized (this) {
+ mVCardParser = (vcardVersion == ImportVCardActivity.VCARD_VERSION_V30 ?
+ new VCardParser_V30(vcardType) :
+ new VCardParser_V21(vcardType));
+ if (mCanceled) {
+ mVCardParser.cancel();
+ }
+ }
+ mVCardParser.parse(is, interpreter);
+
+ successful = true;
+ break;
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
+ } catch (VCardNestedException e) {
+ // This exception should not be thrown here. We should instead handle it
+ // in the preprocessing session in ImportVCardActivity, as we don't try
+ // to detect the type of given vCard here.
+ //
+ // TODO: Handle this case appropriately, which should mean we have to have
+ // code trying to auto-detect the type of given vCard twice (both in
+ // ImportVCardActivity and ImportVCardService).
+ Log.e(LOG_TAG, "Nested Exception is found.");
+ } catch (VCardNotSupportedException e) {
+ Log.e(LOG_TAG, e.getMessage());
+ } catch (VCardVersionException e) {
+ if (i == length - 1) {
+ Log.e(LOG_TAG, "Appropriate version for this vCard is not found.");
+ } else {
+ // We'll try the other (v30) version.
+ }
+ } catch (VCardException e) {
+ Log.e(LOG_TAG, e.getMessage());
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+
+ return successful;
+ }
+
+ public synchronized boolean isReadyForRequest() {
+ return mReadyForRequest;
+ }
+
+ public boolean isCanceled() {
+ return mCanceled;
+ }
+
+ public void cancel() {
+ mCanceled = true;
+ synchronized (this) {
+ if (mVCardParser != null) {
+ mVCardParser.cancel();
+ }
+ }
+ }
+
+ public List<Uri> getCreatedUris() {
+ return mCreatedUris;
+ }
+
+ public List<Uri> getFailedUris() {
+ return mFailedUris;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/ImportProgressNotifier.java b/src/com/android/contacts/vcard/ImportProgressNotifier.java
new file mode 100644
index 0000000..e6f1037
--- /dev/null
+++ b/src/com/android/contacts/vcard/ImportProgressNotifier.java
@@ -0,0 +1,107 @@
+/*
+ * 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.vcard;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.widget.RemoteViews;
+
+import com.android.contacts.ContactsListActivity;
+import com.android.contacts.R;
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntryHandler;
+
+/**
+ * {@link VCardEntryHandler} implementation which lets the system update
+ * the current status of vCard import.
+ */
+public class ImportProgressNotifier implements VCardEntryHandler {
+ private Context mContext;
+ private NotificationManager mNotificationManager;
+
+ private int mCurrentCount;
+ private int mTotalCount;
+
+ public void init(Context context, NotificationManager notificationManager) {
+ mContext = context;
+ mNotificationManager = notificationManager;
+ }
+
+ public void onStart() {
+ }
+
+ public void onEntryCreated(VCardEntry contactStruct) {
+ mCurrentCount++; // 1 origin.
+ if (contactStruct.isIgnorable()) {
+ return;
+ }
+
+ // 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.
+
+ // TODO: should not create this every time?
+ final RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(),
+ R.layout.status_bar_ongoing_event_progress_bar);
+
+ final String title = mContext.getString(R.string.reading_vcard_title);
+
+ String totalCountString;
+ synchronized (this) {
+ totalCountString = String.valueOf(mTotalCount);
+ }
+ final String description = mContext.getString(R.string.progress_notifier_message,
+ String.valueOf(mCurrentCount),
+ totalCountString,
+ contactStruct.getDisplayName());
+
+ remoteViews.setTextViewText(R.id.title, title);
+ remoteViews.setTextViewText(R.id.description, description);
+ remoteViews.setProgressBar(R.id.progress_bar, mTotalCount, mCurrentCount,
+ mTotalCount == -1);
+ final String percentage =
+ mContext.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.tickerText = description;
+ notification.contentView = remoteViews;
+ notification.contentIntent =
+ PendingIntent.getActivity(mContext, 0,
+ new Intent(mContext, ContactsListActivity.class), 0);
+ mNotificationManager.notify(VCardService.IMPORT_NOTIFICATION_ID, notification);
+ }
+
+ public synchronized void addTotalCount(int additionalCount) {
+ mTotalCount += additionalCount;
+ }
+
+ public synchronized void resetTotalCount() {
+ mTotalCount = 0;
+ }
+
+ public void onEnd() {
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/ImportRequest.java b/src/com/android/contacts/vcard/ImportRequest.java
new file mode 100644
index 0000000..5d46166
--- /dev/null
+++ b/src/com/android/contacts/vcard/ImportRequest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.vcard;
+
+import android.accounts.Account;
+import android.net.Uri;
+
+import com.android.vcard.VCardSourceDetector;
+
+/**
+ * Class representing one request for importing vCard (given as a Uri).
+ *
+ * Mainly used when {@link ImportVCardActivity} requests {@link VCardService}
+ * to import some specific Uri.
+ *
+ * Note: This object's accepting only One Uri does NOT mean that
+ * there's only one vCard entry inside the instance, as one Uri often has multiple
+ * vCard entries inside it.
+ */
+public class ImportRequest {
+ /**
+ * Can be null (typically when there's no Account available in the system).
+ */
+ public final Account account;
+ public final Uri uri;
+ /**
+ * Can be {@link VCardSourceDetector#PARSE_TYPE_UNKNOWN}.
+ */
+ public final int estimatedVCardType;
+ /**
+ * Can be null, meaning no preferable charset is available.
+ */
+ public final String estimatedCharset;
+ /**
+ * Assumes that one Uri contains only one version, while there's a (tiny) possibility
+ * we may have two types in one vCard.
+ *
+ * e.g.
+ * BEGIN:VCARD
+ * VERSION:2.1
+ * ...
+ * END:VCARD
+ * BEGIN:VCARD
+ * VERSION:3.0
+ * ...
+ * END:VCARD
+ *
+ * We've never seen this kind of a file, but we may have to cope with it in the future.
+ */
+ public final int vcardVersion;
+
+ /**
+ * The count of vCard entries in {@link #uri}. A receiver of this object can use it
+ * when showing the progress of import. Thus a receiver must be able to torelate this
+ * variable being invalid because of vCard's limitation.
+ *
+ * vCard does not let us know this count without looking over a whole file content,
+ * which means we have to open and scan over {@link #uri} to know this value, while
+ * it may not be opened more than once (Uri does not require it to be opened multiple times
+ * and may become invalid after its close() request).
+ */
+ public final int entryCount;
+ public ImportRequest(Account account,
+ Uri uri, int estimatedType, String estimatedCharset,
+ int vcardVersion, int entryCount) {
+ this.account = account;
+ this.uri = uri;
+ this.estimatedVCardType = estimatedType;
+ this.estimatedCharset = estimatedCharset;
+ this.vcardVersion = vcardVersion;
+ this.entryCount = entryCount;
+ }
+}
diff --git a/src/com/android/contacts/vcard/ImportVCardActivity.java b/src/com/android/contacts/vcard/ImportVCardActivity.java
new file mode 100644
index 0000000..ec63a96
--- /dev/null
+++ b/src/com/android/contacts/vcard/ImportVCardActivity.java
@@ -0,0 +1,978 @@
+/*
+ * 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.vcard;
+
+import android.accounts.Account;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+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.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.RelativeSizeSpan;
+import android.util.Log;
+
+import com.android.contacts.R;
+import com.android.contacts.model.Sources;
+import com.android.contacts.util.AccountSelectionUtil;
+import com.android.vcard.VCardEntryCounter;
+import com.android.vcard.VCardInterpreterCollection;
+import com.android.vcard.VCardParser;
+import com.android.vcard.VCardParser_V21;
+import com.android.vcard.VCardParser_V30;
+import com.android.vcard.VCardSourceDetector;
+import com.android.vcard.exception.VCardException;
+import com.android.vcard.exception.VCardNestedException;
+import com.android.vcard.exception.VCardVersionException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+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.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import java.util.Vector;
+
+/**
+ * The class letting users to import vCard. This includes the UI part for letting them select
+ * an Account and posssibly a file if there's no Uri is given from its caller Activity.
+ *
+ * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
+ * finished (with the method {@link Activity#finish()}) after the import and never reuse
+ * any Dialog in the instance. So this code is careless about the management around managed
+ * dialogs stuffs (like how onCreateDialog() is used).
+ */
+public class ImportVCardActivity extends Activity {
+ private static final String LOG_TAG = "ImportVCardActivity";
+
+ private static final int SELECT_ACCOUNT = 0;
+
+ /* package */ static final String VCARD_URI_ARRAY = "vcard_uri";
+ /* package */ static final String ESTIMATED_VCARD_TYPE_ARRAY = "estimated_vcard_type";
+ /* package */ static final String ESTIMATED_CHARSET_ARRAY = "estimated_charset";
+ /* package */ static final String VCARD_VERSION_ARRAY = "vcard_version";
+ /* package */ static final String ENTRY_COUNT_ARRAY = "entry_count";
+
+ /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0;
+ /* package */ final static int VCARD_VERSION_V21 = 1;
+ /* package */ final static int VCARD_VERSION_V30 = 2;
+
+ private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
+
+ private Account mAccount;
+
+ private String mAction;
+ private Uri mUri;
+
+ private ProgressDialog mProgressDialogForScanVCard;
+ private ProgressDialog mProgressDialogForCacheVCard;
+
+ private List<VCardFile> mAllVCardFileList;
+ private VCardScanThread mVCardScanThread;
+
+ private VCardCacheThread mVCardCacheThread;
+
+ private String mErrorMessage;
+
+ private class CustomConnection implements ServiceConnection {
+ private Messenger mMessenger;
+ /**
+ * Stores {@link ImportRequest} objects until actual connection is established.
+ */
+ private Queue<ImportRequest> mPendingRequests =
+ new LinkedList<ImportRequest>();
+
+ private boolean mConnected = false;
+ private boolean mNeedFinish = false;
+
+ public void doBindService() {
+ bindService(new Intent(ImportVCardActivity.this,
+ VCardService.class), this, Context.BIND_AUTO_CREATE);
+ }
+
+ public void setNeedFinish() {
+ synchronized (this) {
+ mNeedFinish = true;
+ if (mConnected) {
+ unbindService(this);
+ finish();
+ }
+ }
+ }
+
+ public synchronized void requestSend(final ImportRequest parameter) {
+ if (mMessenger != null) {
+ sendMessage(parameter);
+ } else {
+ mPendingRequests.add(parameter);
+ }
+ }
+
+ private void sendMessage(final ImportRequest request) {
+ try {
+ mMessenger.send(Message.obtain(null,
+ VCardService.MSG_IMPORT_REQUEST,
+ request));
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
+ runOnUiThread(new DialogDisplayer(
+ getString(R.string.fail_reason_unknown)));
+ }
+ }
+
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ synchronized (this) {
+ mMessenger = new Messenger(service);
+ // Send pending requests thrown from this Activity before an actual connection
+ // is established.
+ while (!mPendingRequests.isEmpty()) {
+ final ImportRequest parameter = mPendingRequests.poll();
+ if (parameter == null) {
+ throw new NullPointerException();
+ }
+ sendMessage(parameter);
+ }
+ mConnected = true;
+ if (mNeedFinish) {
+ unbindService(this);
+ finish();
+ }
+ }
+ }
+
+ public void onServiceDisconnected(ComponentName name) {
+ synchronized (this) {
+ if (!mPendingRequests.isEmpty()) {
+ Log.w(LOG_TAG, "Some request(s) are dropped.");
+ }
+ // Set to null so that we can detect inappropriate re-connection toward
+ // the Service via NullPointerException;
+ mPendingRequests = null;
+ mMessenger = null;
+ }
+ }
+ }
+
+ private final CustomConnection mConnection = new CustomConnection();
+
+ private static class VCardFile {
+ private final String mName;
+ private final String mCanonicalPath;
+ private final long mLastModified;
+
+ public VCardFile(String name, String canonicalPath, long lastModified) {
+ mName = name;
+ mCanonicalPath = canonicalPath;
+ mLastModified = lastModified;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getCanonicalPath() {
+ return mCanonicalPath;
+ }
+
+ public long getLastModified() {
+ return mLastModified;
+ }
+ }
+
+ // Runs on the UI thread.
+ private class DialogDisplayer implements Runnable {
+ private final int mResId;
+ public DialogDisplayer(int resId) {
+ mResId = resId;
+ }
+ public DialogDisplayer(String errorMessage) {
+ mResId = R.id.dialog_error_with_message;
+ mErrorMessage = errorMessage;
+ }
+ public void run() {
+ showDialog(mResId);
+ }
+ }
+
+ private class CancelListener
+ implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+ }
+
+ private CancelListener mCancelListener = new CancelListener();
+
+ /**
+ * Caches all vCard data into local data directory so that we allow
+ * {@link VCardService} to access all the contents in given Uris, some of
+ * which may not be accessible from other components due to permission problem.
+ * (Activity which gives the Uri may allow only this Activity to access that content,
+ * not the other components like {@link VCardService}.
+ *
+ * We also allow the Service to happen to exit during the vCard import procedure.
+ */
+ private class VCardCacheThread extends Thread
+ implements DialogInterface.OnCancelListener {
+ private static final String CACHE_FILE_PREFIX = "import_tmp_";
+ private boolean mCanceled;
+ private PowerManager.WakeLock mWakeLock;
+ private VCardParser mVCardParser;
+ private final Uri[] mSourceUris;
+
+ public VCardCacheThread(final Uri[] sourceUris) {
+ mSourceUris = sourceUris;
+ final int length = sourceUris.length;
+ final Context context = ImportVCardActivity.this;
+ final 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() {
+ final Context context = ImportVCardActivity.this;
+ final ContentResolver resolver = context.getContentResolver();
+ String errorMessage = null;
+ mWakeLock.acquire();
+ boolean needFinish = true;
+ try {
+ clearOldCache();
+ mConnection.doBindService();
+
+ final int length = mSourceUris.length;
+ // Uris given from caller applications may not be opened twice: consider when
+ // it is not from local storage (e.g. "file:///...") but from some special
+ // provider (e.g. "content://...").
+ // Thus we have to once copy the content of Uri into local storage, and read
+ // it after it. This copy is also useful fro the view of stability of the import,
+ // as we are able to restore the procedure even when it is aborted during it.
+ // Imagine the case the importer encountered memory-low situation when
+ // reading 10th entry of a vCard file.
+ //
+ // We may be able to read content of each vCard file during copying them
+ // to local storage, but currently vCard code does not allow us to do so.
+ for (int i = 0; i < length; i++) {
+ final Uri sourceUri = mSourceUris[i];
+ final Uri localDataUri = copyToLocal(sourceUri, i);
+ if (mCanceled) {
+ break;
+ }
+ if (localDataUri == null) {
+ Log.w(LOG_TAG, "destUri is null");
+ break;
+ }
+ final ImportRequest parameter = constructRequestParameter(localDataUri);
+ if (mCanceled) {
+ return;
+ }
+ mConnection.requestSend(parameter);
+ }
+ } catch (OutOfMemoryError e) {
+ Log.e(LOG_TAG, "OutOfMemoryError");
+ // We should take care of this case since Android devices may have
+ // smaller memory than we usually expect.
+ System.gc();
+ needFinish = false;
+
+ // TODO: call this from connection object.
+ unbindService(mConnection);
+ runOnUiThread(new DialogDisplayer(
+ getString(R.string.fail_reason_low_memory_during_import)));
+ } catch (IOException e) {
+ Log.e(LOG_TAG, e.getMessage());
+ needFinish = false;
+ unbindService(mConnection);
+ runOnUiThread(new DialogDisplayer(
+ getString(R.string.fail_reason_io_error)));
+ } finally {
+ mWakeLock.release();
+ mProgressDialogForCacheVCard.dismiss();
+ if (needFinish) {
+ mConnection.setNeedFinish();
+ }
+ }
+ }
+
+ /**
+ * Copy the content of sourceUri to local storage.
+ */
+ private Uri copyToLocal(final Uri sourceUri, int i) throws IOException {
+ final Context context = ImportVCardActivity.this;
+ final ContentResolver resolver = context.getContentResolver();
+ ReadableByteChannel inputChannel = null;
+ WritableByteChannel outputChannel = null;
+ Uri destUri;
+ try {
+ // XXX: better way to copy stream?
+ {
+ inputChannel = Channels.newChannel(resolver.openInputStream(mSourceUris[i]));
+ final String filename = CACHE_FILE_PREFIX + i + ".vcf";
+ destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString());
+ outputChannel =
+ context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel();
+ final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
+ while (inputChannel.read(buffer) != -1) {
+ if (mCanceled) {
+ Log.d(LOG_TAG, "Canceled during caching " + mSourceUris[i]);
+ return null;
+ }
+ buffer.flip();
+ outputChannel.write(buffer);
+ buffer.compact();
+ }
+ buffer.flip();
+ while (buffer.hasRemaining()) {
+ outputChannel.write(buffer);
+ }
+ }
+ } finally {
+ if (inputChannel != null) {
+ try {
+ inputChannel.close();
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "Failed to close inputChannel.");
+ }
+ }
+ if (outputChannel != null) {
+ try {
+ outputChannel.close();
+ } catch(IOException e) {
+ Log.w(LOG_TAG, "Failed to close outputChannel");
+ }
+ }
+ }
+ return destUri;
+ }
+
+ /**
+ * Reads the Uri once (or twice) and constructs {@link ImportRequest} from
+ * its content.
+ */
+ private ImportRequest constructRequestParameter(final Uri uri) {
+ final ContentResolver resolver =
+ ImportVCardActivity.this.getContentResolver();
+ VCardEntryCounter counter = null;
+ VCardSourceDetector detector = null;
+ VCardInterpreterCollection interpreter = null;
+ int vcardVersion = VCARD_VERSION_V21;
+ try {
+ boolean shouldUseV30 = false;
+ InputStream is;
+
+ is = resolver.openInputStream(uri);
+ mVCardParser = new VCardParser_V21();
+ try {
+ counter = new VCardEntryCounter();
+ detector = new VCardSourceDetector();
+ interpreter =
+ new VCardInterpreterCollection(
+ Arrays.asList(counter, detector));
+ mVCardParser.parse(is, interpreter);
+ } catch (VCardVersionException e1) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+
+ shouldUseV30 = true;
+ is = resolver.openInputStream(uri);
+ mVCardParser = new VCardParser_V30();
+ try {
+ counter = new VCardEntryCounter();
+ detector = new VCardSourceDetector();
+ interpreter =
+ new VCardInterpreterCollection(
+ Arrays.asList(counter, detector));
+ mVCardParser.parse(is, interpreter);
+ } catch (VCardVersionException e2) {
+ throw new VCardException("vCard with unspported version.");
+ }
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21;
+ } catch (VCardNestedException e) {
+ Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
+ // Go through without returning null.
+ } catch (VCardException e) {
+ Log.e(LOG_TAG, e.getMessage());
+ return null;
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
+ return null;
+ }
+ return new ImportRequest(mAccount, uri,
+ detector.getEstimatedType(),
+ detector.getEstimatedCharset(),
+ vcardVersion, counter.getCount());
+ }
+
+ /**
+ * We (currently) don't have any way to clean up cache files used in the previous
+ * import process,
+ * TODO(dmiyakawa): Can we do it after Service being done?
+ */
+ private void clearOldCache() {
+ final Context context = ImportVCardActivity.this;
+ final String[] fileLists = context.fileList();
+ for (String fileName : fileLists) {
+ if (fileName.startsWith(CACHE_FILE_PREFIX)) {
+ Log.d(LOG_TAG, "Remove temporary file: " + fileName);
+ context.deleteFile(fileName);
+ }
+ }
+ }
+
+ 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;
+ public static final int IMPORT_MULTIPLE = 1;
+ public static final int IMPORT_ALL = 2;
+ public static final int IMPORT_TYPE_SIZE = 3;
+
+ private int mCurrentIndex;
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ switch (mCurrentIndex) {
+ case IMPORT_ALL:
+ importVCardFromSDCard(mAllVCardFileList);
+ break;
+ case IMPORT_MULTIPLE:
+ showDialog(R.id.dialog_select_multiple_vcard);
+ break;
+ default:
+ showDialog(R.id.dialog_select_one_vcard);
+ break;
+ }
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+ finish();
+ } else {
+ mCurrentIndex = which;
+ }
+ }
+ }
+
+ private class VCardSelectedListener implements
+ DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener {
+ private int mCurrentIndex;
+ private Set<Integer> mSelectedIndexSet;
+
+ public VCardSelectedListener(boolean multipleSelect) {
+ mCurrentIndex = 0;
+ if (multipleSelect) {
+ mSelectedIndexSet = new HashSet<Integer>();
+ }
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ if (mSelectedIndexSet != null) {
+ List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>();
+ 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));
+ }
+ }
+ importVCardFromSDCard(selectedVCardFileList);
+ } else {
+ importVCardFromSDCard(mAllVCardFileList.get(mCurrentIndex));
+ }
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+ finish();
+ } else {
+ // Some file is selected.
+ mCurrentIndex = which;
+ if (mSelectedIndexSet != null) {
+ if (mSelectedIndexSet.contains(which)) {
+ mSelectedIndexSet.remove(which);
+ } else {
+ mSelectedIndexSet.add(which);
+ }
+ }
+ }
+ }
+
+ public void onClick(DialogInterface dialog, int which, boolean isChecked) {
+ if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) {
+ Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which,
+ mAllVCardFileList.get(which).getCanonicalPath()));
+ } else {
+ onClick(dialog, which);
+ }
+ }
+ }
+
+ /**
+ * Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select
+ * a vCard file is shown. After the choice, VCardReadThread starts running.
+ */
+ private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener {
+ private boolean mCanceled;
+ private boolean mGotIOException;
+ private File mRootDirectory;
+
+ // To avoid recursive link.
+ private Set<String> mCheckedPaths;
+ private PowerManager.WakeLock mWakeLock;
+
+ private class CanceledException extends Exception {
+ }
+
+ public VCardScanThread(File sdcardDirectory) {
+ mCanceled = false;
+ mGotIOException = false;
+ mRootDirectory = sdcardDirectory;
+ mCheckedPaths = new HashSet<String>();
+ PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService(
+ Context.POWER_SERVICE);
+ mWakeLock = powerManager.newWakeLock(
+ PowerManager.SCREEN_DIM_WAKE_LOCK |
+ PowerManager.ON_AFTER_RELEASE, LOG_TAG);
+ }
+
+ @Override
+ public void run() {
+ mAllVCardFileList = new Vector<VCardFile>();
+ try {
+ mWakeLock.acquire();
+ getVCardFileRecursively(mRootDirectory);
+ } catch (CanceledException e) {
+ mCanceled = true;
+ } catch (IOException e) {
+ mGotIOException = true;
+ } finally {
+ mWakeLock.release();
+ }
+
+ if (mCanceled) {
+ mAllVCardFileList = null;
+ }
+
+ mProgressDialogForScanVCard.dismiss();
+ mProgressDialogForScanVCard = null;
+
+ if (mGotIOException) {
+ runOnUiThread(new DialogDisplayer(R.id.dialog_io_exception));
+ } else if (mCanceled) {
+ finish();
+ } else {
+ int size = mAllVCardFileList.size();
+ final Context context = ImportVCardActivity.this;
+ if (size == 0) {
+ runOnUiThread(new DialogDisplayer(R.id.dialog_vcard_not_found));
+ } else {
+ startVCardSelectAndImport();
+ }
+ }
+ }
+
+ private void getVCardFileRecursively(File directory)
+ throws CanceledException, IOException {
+ if (mCanceled) {
+ throw new CanceledException();
+ }
+
+ // e.g. secured directory may return null toward listFiles().
+ final File[] files = directory.listFiles();
+ if (files == null) {
+ Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")");
+ return;
+ }
+ for (File file : directory.listFiles()) {
+ if (mCanceled) {
+ throw new CanceledException();
+ }
+ String canonicalPath = file.getCanonicalPath();
+ if (mCheckedPaths.contains(canonicalPath)) {
+ continue;
+ }
+
+ mCheckedPaths.add(canonicalPath);
+
+ if (file.isDirectory()) {
+ getVCardFileRecursively(file);
+ } else if (canonicalPath.toLowerCase().endsWith(".vcf") &&
+ file.canRead()){
+ String fileName = file.getName();
+ VCardFile vcardFile = new VCardFile(
+ fileName, canonicalPath, file.lastModified());
+ mAllVCardFileList.add(vcardFile);
+ }
+ }
+ }
+
+ public void onCancel(DialogInterface dialog) {
+ mCanceled = true;
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_NEGATIVE) {
+ mCanceled = true;
+ }
+ }
+ }
+
+ private void startVCardSelectAndImport() {
+ int size = mAllVCardFileList.size();
+ 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 {
+ runOnUiThread(new DialogDisplayer(R.id.dialog_select_one_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) {
+ importVCard(new Uri[] {Uri.parse("file://" + vcardFile.getCanonicalPath())});
+ }
+
+ private void importVCard(final Uri uri) {
+ importVCard(new Uri[] {uri});
+ }
+
+ private void importVCard(final String[] uriStrings) {
+ final int length = uriStrings.length;
+ final Uri[] uris = new Uri[length];
+ for (int i = 0; i < length; i++) {
+ uris[i] = Uri.parse(uriStrings[i]);
+ }
+ importVCard(uris);
+ }
+
+ private void importVCard(final Uri[] uris) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ mVCardCacheThread = new VCardCacheThread(uris);
+ showDialog(R.id.dialog_cache_vcard);
+ }
+ });
+ }
+
+ private Dialog getSelectImportTypeDialog() {
+ 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);
+
+ final String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE];
+ items[ImportTypeSelectedListener.IMPORT_ONE] =
+ getString(R.string.import_one_vcard_string);
+ items[ImportTypeSelectedListener.IMPORT_MULTIPLE] =
+ getString(R.string.import_multiple_vcard_string);
+ items[ImportTypeSelectedListener.IMPORT_ALL] =
+ getString(R.string.import_all_vcard_string);
+ builder.setSingleChoiceItems(items, ImportTypeSelectedListener.IMPORT_ONE, listener);
+ return builder.create();
+ }
+
+ private Dialog getVCardFileSelectDialog(boolean multipleSelect) {
+ 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");
+ for (int i = 0; i < size; i++) {
+ VCardFile vcardFile = mAllVCardFileList.get(i);
+ SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
+ stringBuilder.append(vcardFile.getName());
+ stringBuilder.append('\n');
+ int indexToBeSpanned = stringBuilder.length();
+ // Smaller date text looks better, since each file name becomes easier to read.
+ // The value set to RelativeSizeSpan is arbitrary. You can change it to any other
+ // value (but the value bigger than 1.0f would not make nice appearance :)
+ stringBuilder.append(
+ "(" + dateFormat.format(new Date(vcardFile.getLastModified())) + ")");
+ stringBuilder.setSpan(
+ new RelativeSizeSpan(0.7f), indexToBeSpanned, stringBuilder.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ items[i] = stringBuilder;
+ }
+ if (multipleSelect) {
+ builder.setMultiChoiceItems(items, (boolean[])null, listener);
+ } else {
+ builder.setSingleChoiceItems(items, 0, listener);
+ }
+ return builder.create();
+ }
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ String accountName = null;
+ String accountType = null;
+ final Intent intent = getIntent();
+ if (intent != null) {
+ accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
+ accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
+ mAction = intent.getAction();
+ mUri = intent.getData();
+ } else {
+ Log.e(LOG_TAG, "intent does not exist");
+ }
+
+ if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+ mAccount = new Account(accountName, accountType);
+ } else {
+ final Sources sources = Sources.getInstance(this);
+ final List<Account> accountList = sources.getAccounts(true);
+ if (accountList.size() == 0) {
+ mAccount = null;
+ } else if (accountList.size() == 1) {
+ mAccount = accountList.get(0);
+ } else {
+ startActivityForResult(new Intent(this, SelectAccountActivity.class),
+ SELECT_ACCOUNT);
+ return;
+ }
+ }
+
+ startImport(mAction, mUri);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (requestCode == SELECT_ACCOUNT) {
+ if (resultCode == RESULT_OK) {
+ mAccount = new Account(
+ intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
+ intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE));
+ startImport(mAction, mUri);
+ } else {
+ if (resultCode != RESULT_CANCELED) {
+ Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode);
+ }
+ finish();
+ }
+ }
+ }
+
+ private void startImport(String action, Uri uri) {
+ Log.d(LOG_TAG, "action = " + action + " ; path = " + uri);
+
+ if (uri != null) {
+ importVCard(uri);
+ } else {
+ doScanExternalStorageAndImportVCard();
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int resId, Bundle bundle) {
+ switch (resId) {
+ case R.string.import_from_sdcard: {
+ if (mAccountSelectionListener == null) {
+ throw new NullPointerException(
+ "mAccountSelectionListener must not be null.");
+ }
+ return AccountSelectionUtil.getSelectAccountDialog(this, resId,
+ mAccountSelectionListener, mCancelListener);
+ }
+ case R.id.dialog_searching_vcard: {
+ if (mProgressDialogForScanVCard == null) {
+ String title = getString(R.string.searching_vcard_title);
+ String message = getString(R.string.searching_vcard_message);
+ mProgressDialogForScanVCard =
+ ProgressDialog.show(this, title, message, true, false);
+ mProgressDialogForScanVCard.setOnCancelListener(mVCardScanThread);
+ mVCardScanThread.start();
+ }
+ return mProgressDialogForScanVCard;
+ }
+ case R.id.dialog_sdcard_not_found: {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.no_sdcard_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.no_sdcard_message)
+ .setOnCancelListener(mCancelListener)
+ .setPositiveButton(android.R.string.ok, mCancelListener);
+ return builder.create();
+ }
+ case R.id.dialog_vcard_not_found: {
+ String message = (getString(R.string.scanning_sdcard_failed_message,
+ getString(R.string.fail_reason_no_vcard_file)));
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.scanning_sdcard_failed_title)
+ .setMessage(message)
+ .setOnCancelListener(mCancelListener)
+ .setPositiveButton(android.R.string.ok, mCancelListener);
+ return builder.create();
+ }
+ case R.id.dialog_select_import_type: {
+ return getSelectImportTypeDialog();
+ }
+ case R.id.dialog_select_multiple_vcard: {
+ return getVCardFileSelectDialog(true);
+ }
+ case R.id.dialog_select_one_vcard: {
+ return getVCardFileSelectDialog(false);
+ }
+ case R.id.dialog_cache_vcard: {
+ if (mProgressDialogForCacheVCard == null) {
+ final String title = getString(R.string.caching_vcard_title);
+ final String message = getString(R.string.caching_vcard_message);
+ mProgressDialogForCacheVCard = new ProgressDialog(this);
+ mProgressDialogForCacheVCard.setTitle(title);
+ mProgressDialogForCacheVCard.setMessage(message);
+ mProgressDialogForCacheVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ mProgressDialogForCacheVCard.setOnCancelListener(mVCardCacheThread);
+ mVCardCacheThread.start();
+ }
+ return mProgressDialogForCacheVCard;
+ }
+ case R.id.dialog_io_exception: {
+ String message = (getString(R.string.scanning_sdcard_failed_message,
+ getString(R.string.fail_reason_io_error)));
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.scanning_sdcard_failed_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(message)
+ .setOnCancelListener(mCancelListener)
+ .setPositiveButton(android.R.string.ok, mCancelListener);
+ return builder.create();
+ }
+ case R.id.dialog_error_with_message: {
+ String message = mErrorMessage;
+ if (TextUtils.isEmpty(message)) {
+ Log.e(LOG_TAG, "Error message is null while it must not.");
+ message = getString(R.string.fail_reason_unknown);
+ }
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(getString(R.string.reading_vcard_failed_title))
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(message)
+ .setOnCancelListener(mCancelListener)
+ .setPositiveButton(android.R.string.ok, mCancelListener);
+ return builder.create();
+ }
+ }
+
+ return super.onCreateDialog(resId, bundle);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ // 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
+ // screen back to the caller Activity.
+ if (!isFinishing()) {
+ finish();
+ }
+ }
+
+ /**
+ * Scans vCard in external storage (typically SDCard) and tries to import it.
+ * - When there's no SDCard available, an error dialog is shown.
+ * - When multiple vCard files are available, asks a user to select one.
+ */
+ private void doScanExternalStorageAndImportVCard() {
+ // TODO: should use getExternalStorageState().
+ final File file = Environment.getExternalStorageDirectory();
+ if (!file.exists() || !file.isDirectory() || !file.canRead()) {
+ showDialog(R.id.dialog_sdcard_not_found);
+ } else {
+ mVCardScanThread = new VCardScanThread(file);
+ showDialog(R.id.dialog_searching_vcard);
+ }
+ }
+}
diff --git a/src/com/android/contacts/vcard/SelectAccountActivity.java b/src/com/android/contacts/vcard/SelectAccountActivity.java
new file mode 100644
index 0000000..dfd5196
--- /dev/null
+++ b/src/com/android/contacts/vcard/SelectAccountActivity.java
@@ -0,0 +1,107 @@
+/*
+ * 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.vcard;
+
+import android.accounts.Account;
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.contacts.R;
+import com.android.contacts.model.Sources;
+import com.android.contacts.util.AccountSelectionUtil;
+
+import java.util.List;
+
+public class SelectAccountActivity extends Activity {
+ private static final String LOG_TAG = "SelectAccountActivity";
+
+ public static final String ACCOUNT_NAME = "account_name";
+ public static final String ACCOUNT_TYPE = "account_type";
+
+ private class CancelListener
+ implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+ }
+
+ private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ // There's three possibilities:
+ // - more than one accounts -> ask the user
+ // - just one account -> use the account without asking the user
+ // - no account -> use phone-local storage without asking the user
+ final int resId = R.string.import_from_sdcard;
+ final Sources sources = Sources.getInstance(this);
+ final List<Account> accountList = sources.getAccounts(true);
+ if (accountList.size() == 0) {
+ Log.w(LOG_TAG, "Account does not exist");
+ finish();
+ } else if (accountList.size() == 1) {
+ final Account account = accountList.get(0);
+ final Intent intent = new Intent();
+ intent.putExtra(ACCOUNT_NAME, account.name);
+ intent.putExtra(ACCOUNT_TYPE, account.type);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+ // Multiple accounts. Let users to select one.
+ mAccountSelectionListener =
+ new AccountSelectionUtil.AccountSelectedListener(
+ this, accountList, resId) {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ final Account account = mAccountList.get(which);
+ final Intent intent = new Intent();
+ intent.putExtra(ACCOUNT_NAME, account.name);
+ intent.putExtra(ACCOUNT_TYPE, account.type);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+ };
+ showDialog(resId);
+ return;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int resId, Bundle bundle) {
+ switch (resId) {
+ case R.string.import_from_sdcard: {
+ if (mAccountSelectionListener == null) {
+ throw new NullPointerException(
+ "mAccountSelectionListener must not be null.");
+ }
+ return AccountSelectionUtil.getSelectAccountDialog(this, resId,
+ mAccountSelectionListener,
+ new CancelListener());
+ }
+ }
+ return super.onCreateDialog(resId, bundle);
+ }
+}
diff --git a/src/com/android/contacts/vcard/ThreadStarter.java b/src/com/android/contacts/vcard/ThreadStarter.java
new file mode 100644
index 0000000..d7adad6
--- /dev/null
+++ b/src/com/android/contacts/vcard/ThreadStarter.java
@@ -0,0 +1,20 @@
+/*
+ * 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.vcard;
+
+public interface ThreadStarter {
+ public void start();
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/VCardService.java b/src/com/android/contacts/vcard/VCardService.java
new file mode 100644
index 0000000..58e1333
--- /dev/null
+++ b/src/com/android/contacts/vcard/VCardService.java
@@ -0,0 +1,92 @@
+/*
+ * 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.vcard;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.contacts.R;
+
+/**
+ * The class responsible for importing vCard from one ore multiple Uris.
+ */
+public class VCardService extends Service {
+ private final static String LOG_TAG = "ImportVCardService";
+
+ /* package */ static final int MSG_IMPORT_REQUEST = 1;
+ /* package */ static final int MSG_EXPORT_REQUEST = 2;
+
+ /* package */ static final int IMPORT_NOTIFICATION_ID = 1000;
+ /* package */ static final int EXPORT_NOTIFICATION_ID = 1001;
+
+ /**
+ * Small vCard file is imported soon, so any meassage saying "vCard import started" is
+ * not needed. We show the message when the size of vCard is larger than this constant.
+ */
+ private static final int IMPORT_NOTIFICATION_THRESHOLD = 10;
+
+ public class ImportRequestHandler extends Handler {
+ private final ImportProcessor mImportProcessor =
+ new ImportProcessor(VCardService.this);
+ private final ExportProcessor mExportProcessor =
+ new ExportProcessor(VCardService.this);
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_IMPORT_REQUEST: {
+ final ImportRequest parameter = (ImportRequest)msg.obj;
+ mImportProcessor.pushRequest(parameter);
+ if (parameter.entryCount > IMPORT_NOTIFICATION_THRESHOLD) {
+ Toast.makeText(VCardService.this,
+ getString(R.string.vcard_importer_start_message),
+ Toast.LENGTH_LONG).show();
+ }
+ break;
+ }
+ case MSG_EXPORT_REQUEST: {
+ final ExportRequest parameter = (ExportRequest)msg.obj;
+ mExportProcessor.pushRequest(parameter);
+ Toast.makeText(VCardService.this,
+ getString(R.string.vcard_exporter_start_message),
+ Toast.LENGTH_LONG).show();
+ break;
+ }
+ default: {
+ Log.e(LOG_TAG, "Unknown request type: " + msg.what);
+ super.hasMessages(msg.what);
+ }
+ }
+ }
+ }
+
+ private Messenger mMessenger = new Messenger(new ImportRequestHandler());
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int id) {
+ return START_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mMessenger.getBinder();
+ }
+}
diff --git a/src/com/android/contacts/views/ContactLoader.java b/src/com/android/contacts/views/ContactLoader.java
new file mode 100644
index 0000000..87c3a34
--- /dev/null
+++ b/src/com/android/contacts/views/ContactLoader.java
@@ -0,0 +1,513 @@
+/*
+ * 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;
+
+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.content.Loader;
+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 static final String TAG = "ContactLoader";
+
+ private Uri mLookupUri;
+ private Result mContact;
+ private ForceLoadContentObserver mObserver;
+ private boolean mDestroyed;
+
+ public interface Listener {
+ 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();
+
+ /**
+ * Singleton instance that represents an error, e.g. because of an invalid Uri
+ * TODO: We should come up with something nicer here. Maybe use an Either type so
+ * that we can capture the Exception?
+ */
+ public static final Result ERROR = new Result();
+
+ private final Uri mLookupUri;
+ private final String mLookupKey;
+ private final Uri mUri;
+ private final long mId;
+ private final long mNameRawContactId;
+ private final int mDisplayNameSource;
+ private final long mPhotoId;
+ private final String mDisplayName;
+ private final String mPhoneticName;
+ private final boolean mStarred;
+ private final Integer mPresence;
+ private final ArrayList<Entity> mEntities;
+ private final HashMap<Long, DataStatus> mStatuses;
+ private final String mStatus;
+ private final Long mStatusTimestamp;
+ private final Integer mStatusLabel;
+ private final String mStatusResPackage;
+
+ /**
+ * 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;
+ mPhotoId = -1;
+ mDisplayName = null;
+ mPhoneticName = null;
+ mStarred = false;
+ mPresence = null;
+ mStatus = null;
+ mStatusTimestamp = null;
+ mStatusLabel = null;
+ mStatusResPackage = null;
+ }
+
+ /**
+ * Constructor to call when contact was found
+ */
+ private Result(Uri lookupUri, String lookupKey, Uri uri, long id, long nameRawContactId,
+ int displayNameSource, long photoId, String displayName, String phoneticName,
+ boolean starred, Integer presence, String status, Long statusTimestamp,
+ Integer statusLabel, String statusResPackage) {
+ mLookupUri = lookupUri;
+ mLookupKey = lookupKey;
+ mUri = uri;
+ mId = id;
+ mEntities = new ArrayList<Entity>();
+ mStatuses = new HashMap<Long, DataStatus>();
+ mNameRawContactId = nameRawContactId;
+ mDisplayNameSource = displayNameSource;
+ mPhotoId = photoId;
+ mDisplayName = displayName;
+ mPhoneticName = phoneticName;
+ mStarred = starred;
+ mPresence = presence;
+ mStatus = status;
+ mStatusTimestamp = statusTimestamp;
+ mStatusLabel = statusLabel;
+ mStatusResPackage = statusResPackage;
+ }
+
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+ public String getLookupKey() {
+ return mLookupKey;
+ }
+ public Uri getUri() {
+ return mUri;
+ }
+ public long getId() {
+ return mId;
+ }
+ public long getNameRawContactId() {
+ return mNameRawContactId;
+ }
+ public int getDisplayNameSource() {
+ return mDisplayNameSource;
+ }
+ public long getPhotoId() {
+ return mPhotoId;
+ }
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+ public String getPhoneticName() {
+ return mPhoneticName;
+ }
+ public boolean getStarred() {
+ return mStarred;
+ }
+ public Integer getPresence() {
+ return mPresence;
+ }
+ public String getStatus() {
+ return mStatus;
+ }
+ public Long getStatusTimestamp() {
+ return mStatusTimestamp;
+ }
+ public Integer getStatusLabel() {
+ return mStatusLabel;
+ }
+ public String getStatusResPackage() {
+ return mStatusResPackage;
+ }
+ public ArrayList<Entity> getEntities() {
+ return mEntities;
+ }
+ public HashMap<Long, DataStatus> getStatuses() {
+ return mStatuses;
+ }
+ }
+
+ private 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;
+ }
+
+ private interface ContactQuery {
+ //Projection used for the summary info in the header.
+ final static String[] COLUMNS = new String[] {
+ Contacts.NAME_RAW_CONTACT_ID,
+ Contacts.DISPLAY_NAME_SOURCE,
+ Contacts.LOOKUP_KEY,
+ Contacts.DISPLAY_NAME,
+ Contacts.PHONETIC_NAME,
+ Contacts.PHOTO_ID,
+ Contacts.STARRED,
+ Contacts.CONTACT_PRESENCE,
+ Contacts.CONTACT_STATUS,
+ Contacts.CONTACT_STATUS_TIMESTAMP,
+ Contacts.CONTACT_STATUS_RES_PACKAGE,
+ Contacts.CONTACT_STATUS_LABEL,
+ };
+ final static int NAME_RAW_CONTACT_ID = 0;
+ final static int DISPLAY_NAME_SOURCE = 1;
+ final static int LOOKUP_KEY = 2;
+ final static int DISPLAY_NAME = 3;
+ final static int PHONETIC_NAME = 4;
+ final static int PHOTO_ID = 5;
+ final static int STARRED = 6;
+ final static int CONTACT_PRESENCE = 7;
+ final static int CONTACT_STATUS = 8;
+ final static int CONTACT_STATUS_TIMESTAMP = 9;
+ final static int CONTACT_STATUS_RES_PACKAGE = 10;
+ final static int CONTACT_STATUS_LABEL = 11;
+ }
+
+ private final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+
+ @Override
+ protected Result doInBackground(Void... args) {
+ try {
+ final ContentResolver resolver = getContext().getContentResolver();
+ final Uri uriCurrentFormat = ensureIsContactUri(resolver, 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;
+ } catch (Exception e) {
+ Log.w(TAG, "Error loading the contact: " + e.getMessage());
+ return Result.ERROR;
+ }
+ }
+
+ /**
+ * Transforms the given Uri and returns a Lookup-Uri that represents the contact.
+ * For legacy contacts, a raw-contact lookup is performed.
+ * @param resolver
+ */
+ private Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri) {
+ if (uri == null) throw new IllegalArgumentException("uri must not be null");
+
+ final String authority = uri.getAuthority();
+
+ // Current Style Uri?
+ if (ContactsContract.AUTHORITY.equals(authority)) {
+ final String type = resolver.getType(uri);
+ // Contact-Uri? Good, return it
+ if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
+ return uri;
+ }
+
+ // RawContact-Uri? Transform it to ContactUri
+ if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) {
+ final long rawContactId = ContentUris.parseId(uri);
+ return RawContacts.getContactLookupUri(getContext().getContentResolver(),
+ ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+ }
+
+ // Anything else? We don't know what this is
+ throw new IllegalArgumentException("uri format is unknown");
+ }
+
+ // 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(resolver,
+ ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+ }
+
+ throw new IllegalArgumentException("uri authority 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 Cursor cursor = resolver.query(contactUri, ContactQuery.COLUMNS, 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;
+ }
+ final String lookupKey = cursor.getString(ContactQuery.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;
+ }
+
+ final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
+ final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
+ final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
+ final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
+ final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
+ final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
+ final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
+ ? null
+ : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
+ final String status = cursor.getString(ContactQuery.CONTACT_STATUS);
+ final Long statusTimestamp = cursor.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP)
+ ? null
+ : cursor.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP);
+ final Integer statusLabel = cursor.isNull(ContactQuery.CONTACT_STATUS_LABEL)
+ ? null
+ : cursor.getInt(ContactQuery.CONTACT_STATUS_LABEL);
+ final String statusResPackage = cursor.getString(
+ ContactQuery.CONTACT_STATUS_RES_PACKAGE);
+
+ return new Result(lookupUri, lookupKey, contactUri, uriContactId, nameRawContactId,
+ displayNameSource, photoId, displayName, phoneticName, starred, presence,
+ status, statusTimestamp, statusLabel, statusResPackage);
+ } 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;
+ mLookupUri = result.getLookupUri();
+ if (result != null) {
+ unregisterObserver();
+ if (mObserver == null) {
+ mObserver = new ForceLoadContentObserver();
+ }
+ Log.i(TAG, "Registering content observer for " + mLookupUri);
+
+ if (result != Result.ERROR && result != Result.NOT_FOUND) {
+ getContext().getContentResolver().registerContentObserver(mLookupUri, true,
+ mObserver);
+ }
+ deliverResult(result);
+ }
+ }
+ }
+
+ private void unregisterObserver() {
+ if (mObserver != null) {
+ getContext().getContentResolver().unregisterContentObserver(mObserver);
+ mObserver = null;
+ }
+ }
+
+ 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() {
+ final LoadContactTask task = new LoadContactTask();
+ task.execute((Void[])null);
+ }
+
+ @Override
+ public void stopLoading() {
+ mContact = null;
+ }
+
+ @Override
+ public void destroy() {
+ mContact = null;
+ mDestroyed = true;
+ }
+}
diff --git a/src/com/android/contacts/views/ContactSaveService.java b/src/com/android/contacts/views/ContactSaveService.java
new file mode 100644
index 0000000..936b8a4
--- /dev/null
+++ b/src/com/android/contacts/views/ContactSaveService.java
@@ -0,0 +1,59 @@
+/*
+ * 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.views;
+
+import android.app.IntentService;
+import android.content.ContentProviderOperation;
+import android.content.Intent;
+import android.content.OperationApplicationException;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+public class ContactSaveService extends IntentService {
+ private static final String TAG = "ContactSaveService";
+
+ public static final String EXTRA_OPERATIONS = "Operations";
+
+ public ContactSaveService() {
+ super(TAG);
+ setIntentRedelivery(true);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ final Parcelable[] operationsArray = intent.getParcelableArrayExtra(EXTRA_OPERATIONS);
+
+ // We have to cast each item individually here
+ final ArrayList<ContentProviderOperation> operations =
+ new ArrayList<ContentProviderOperation>(operationsArray.length);
+ for (Parcelable p : operationsArray) {
+ operations.add((ContentProviderOperation) p);
+ }
+
+ try {
+ getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error saving", e);
+ } catch (OperationApplicationException e) {
+ Log.e(TAG, "Error saving", e);
+ }
+ }
+}
diff --git a/src/com/android/contacts/views/detail/ContactDetailFragment.java b/src/com/android/contacts/views/detail/ContactDetailFragment.java
new file mode 100644
index 0000000..49471cc
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactDetailFragment.java
@@ -0,0 +1,1073 @@
+/*
+ * 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.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.util.Constants;
+import com.android.contacts.util.DataStatus;
+import com.android.contacts.views.ContactLoader;
+import com.android.internal.telephony.ITelephony;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.LoaderManagingFragment;
+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.Loader;
+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.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+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.OnClickListener;
+import android.view.View.OnCreateContextMenuListener;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AdapterView.OnItemClickListener;
+
+import java.util.ArrayList;
+
+public class ContactDetailFragment extends LoaderManagingFragment<ContactLoader.Result>
+ implements OnCreateContextMenuListener, OnItemClickListener {
+ private static final String TAG = "ContactDetailFragment";
+
+ private static final int MENU_ITEM_MAKE_DEFAULT = 3;
+
+ private static final int LOADER_DETAILS = 1;
+
+ private Context mContext;
+ private Uri mLookupUri;
+ private Listener mListener;
+
+ private boolean mIsInitialized;
+
+ private ContactLoader.Result mContactData;
+ private ContactDetailHeaderView mHeaderView;
+ private ListView mListView;
+ private boolean mShowSmsLinksForAllPhones;
+ private ViewAdapter mAdapter;
+ private Uri mPrimaryPhoneUri = null;
+
+ private int mReadOnlySourcesCnt;
+ private int mWritableSourcesCnt;
+ private boolean mAllRestricted;
+ private final 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>>();
+ private LayoutInflater mInflater;
+
+ public ContactDetailFragment() {
+ // Explicit constructor for inflation
+
+ // 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);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mContext = activity;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ final View view = inflater.inflate(R.layout.contact_detail_fragment, container, false);
+
+ setHasOptionsMenu(true);
+
+ mInflater = inflater;
+
+ mHeaderView = (ContactDetailHeaderView) view.findViewById(R.id.contact_header_widget);
+ mHeaderView.setExcludeMimes(new String[] {
+ Contacts.CONTENT_ITEM_TYPE
+ });
+ mHeaderView.setListener(mHeaderViewListener);
+
+ mListView = (ListView) view.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 = view.findViewById(android.R.id.empty);
+
+ //TODO Read this value from a preference
+ mShowSmsLinksForAllPhones = true;
+
+ return view;
+ }
+
+ public void setListener(Listener value) {
+ mListener = value;
+ }
+
+ public void loadUri(Uri lookupUri) {
+ mLookupUri = lookupUri;
+ if (mIsInitialized) startLoading(LOADER_DETAILS, null);
+ }
+
+ @Override
+ protected void onInitializeLoaders() {
+ mIsInitialized = true;
+ if (mLookupUri != null) startLoading(LOADER_DETAILS, null);
+ }
+
+ @Override
+ protected Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_DETAILS: {
+ return new ContactLoader(mContext, mLookupUri);
+ }
+ default: {
+ Log.wtf(TAG, "Unknown ID in onCreateLoader: " + id);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onLoadFinished(Loader<ContactLoader.Result> loader,
+ ContactLoader.Result data) {
+ final int id = loader.getId();
+ switch (id) {
+ case LOADER_DETAILS:
+ if (data == ContactLoader.Result.NOT_FOUND) {
+ // Item has been deleted
+ Log.i(TAG, "No contact found. Closing activity");
+ mListener.onContactNotFound();
+ return;
+ }
+ mContactData = data;
+ bindData();
+ break;
+ default: {
+ Log.wtf(TAG, "Unknown ID in onLoadFinished: " + id);
+ }
+ }
+ }
+
+ private void bindData() {
+ // Set the header
+ mHeaderView.loadData(mContactData);
+
+ // 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();
+ mListView.setAdapter(mAdapter);
+ } else {
+ mAdapter.notifyDataSetChanged();
+ }
+ 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();
+
+ // TODO: This should be done in the background thread
+ 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 (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ // Always ignore the name. It is shown in the header if set
+ } else 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);
+ }
+ }
+ }
+ }
+ }
+
+ private 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();
+ }
+
+ private 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.
+ */
+ private static class ViewEntry implements Collapsible<ViewEntry> {
+ // Copied from baseclass
+ public int type = -1;
+ public String label;
+ public String data;
+ public Uri uri;
+ public long id = 0;
+ public int maxLines = 1;
+ public String mimetype;
+
+ 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.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;
+ }
+
+ private final class ViewAdapter extends BaseAdapter {
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final ViewEntry entry = getEntry(position);
+ 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(mSecondaryActionClickListener);
+ viewCache.secondaryActionDivider = v.findViewById(R.id.divider);
+ v.setTag(viewCache);
+ }
+
+ // Bind the data to the view
+ bindView(v, entry);
+ return v;
+ }
+
+ 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
+ final 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
+ final Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
+ mContext, entry.presence);
+ final ImageView presenceIconView = views.presenceIcon;
+ if (presenceIcon != null) {
+ presenceIconView.setImageDrawable(presenceIcon);
+ presenceIconView.setVisibility(View.VISIBLE);
+ } else {
+ presenceIconView.setVisibility(View.GONE);
+ }
+
+ // Set the secondary action button
+ final 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);
+ }
+ }
+
+ private OnClickListener mSecondaryActionClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ if (mListener == null) return;
+ if (v == null) return;
+ final ViewEntry entry = (ViewEntry) v.getTag();
+ if (entry == null) return;
+ final Intent intent = entry.secondaryIntent;
+ if (intent == null) return;
+ mListener.onItemClicked(intent);
+ }
+ };
+
+ public int getCount() {
+ int count = 0;
+ final int numSections = mSections.size();
+ for (int i = 0; i < numSections; i++) {
+ final ArrayList<ViewEntry> section = mSections.get(i);
+ count += section.size();
+ }
+ return count;
+ }
+
+ public Object getItem(int position) {
+ return getEntry(position);
+ }
+
+ public long getItemId(int position) {
+ final ViewEntry entry = getEntry(position);
+ if (entry != null) {
+ return entry.id;
+ } else {
+ return -1;
+ }
+ }
+
+ private ViewEntry getEntry(int position) {
+ final int numSections = mSections.size();
+ for (int i = 0; i < numSections; i++) {
+ final ArrayList<ViewEntry> section = mSections.get(i);
+ final int sectionSize = section.size();
+ if (position < sectionSize) {
+ return section.get(position);
+ }
+ position -= sectionSize;
+ }
+ return null;
+ }
+ }
+
+ public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
+ inflater.inflate(R.menu.view, menu);
+ }
+
+ public void 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);
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_edit: {
+ mListener.onEditRequested(mLookupUri);
+ break;
+ }
+ 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() {
+ final int id;
+ if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
+ id = R.id.detail_dialog_confirm_readonly_delete;
+ } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
+ id = R.id.detail_dialog_confirm_readonly_hide;
+ } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
+ id = R.id.detail_dialog_confirm_multiple_delete;
+ } else {
+ id = R.id.detail_dialog_confirm_delete;
+ }
+ if (mListener != null) mListener.onDialogRequested(id, null);
+ }
+
+ 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;
+ }
+
+ final ViewEntry entry = mAdapter.getEntry(info.position);
+ 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 (mListener == null) return;
+ final ViewEntry entry = mAdapter.getEntry(position);
+ if (entry == null) return;
+ final Intent intent = entry.intent;
+ if (intent == null) return;
+ mListener.onItemClicked(intent);
+ }
+
+ private final DialogInterface.OnClickListener mDeleteListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mContext.getContentResolver().delete(mContactData.getLookupUri(), null, null);
+ }
+ };
+
+ public Dialog onCreateDialog(int id, Bundle bundle) {
+ switch (id) {
+ case R.id.detail_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 R.id.detail_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 R.id.detail_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 R.id.detail_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:
+ return null;
+ }
+ }
+
+ 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 mAdapter.getEntry(info.position);
+ }
+
+ 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 = mAdapter.getEntry(index);
+ 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 false;
+ }
+
+ private ContactDetailHeaderView.Listener mHeaderViewListener =
+ new ContactDetailHeaderView.Listener() {
+ public void onDisplayNameClick(View view) {
+ }
+
+ public void onEditClicked() {
+ if (mListener != null) mListener.onEditRequested(mLookupUri);
+ }
+
+ public void onPhotoClick(View view) {
+ }
+ };
+
+
+ public static interface Listener {
+ /**
+ * Contact was not found, so somehow close this fragment.
+ */
+ public void onContactNotFound();
+
+ /**
+ * User decided to go to Edit-Mode
+ */
+ public void onEditRequested(Uri lookupUri);
+
+ /**
+ * User clicked a single item (e.g. mail)
+ */
+ public void onItemClicked(Intent intent);
+
+ /**
+ * Show a dialog using the globally unique id
+ */
+ public void onDialogRequested(int id, Bundle bundle);
+ }
+}
diff --git a/src/com/android/contacts/views/detail/ContactDetailHeaderView.java b/src/com/android/contacts/views/detail/ContactDetailHeaderView.java
new file mode 100644
index 0000000..85afd2d
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactDetailHeaderView.java
@@ -0,0 +1,384 @@
+/*
+ * 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.views.detail;
+
+import com.android.contacts.R;
+import com.android.contacts.views.ContactLoader;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.content.Entity.NamedContentValues;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.ImageButton;
+import android.widget.QuickContactBadge;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * Header for displaying a title bar with contact info. You
+ * can bind specific values by calling
+ * {@link ContactDetailHeaderView#loadData(com.android.contacts.views.ContactLoader.Result)}
+ */
+public class ContactDetailHeaderView extends FrameLayout implements View.OnClickListener {
+
+ private static final String TAG = "ContactDetailHeaderView";
+
+ private TextView mDisplayNameView;
+ private TextView mPhoneticNameView;
+ private CheckBox mStarredView;
+ private QuickContactBadge mPhotoView;
+ private ImageView mPresenceView;
+ private TextView mStatusView;
+ private TextView mStatusAttributionView;
+ private ImageButton mEditButton;
+
+ private Uri mContactUri;
+ private Listener mListener;
+
+ /**
+ * Interface for callbacks invoked when the user interacts with a header.
+ */
+ public interface Listener {
+ public void onPhotoClick(View view);
+ public void onDisplayNameClick(View view);
+ public void onEditClicked();
+ }
+
+ public ContactDetailHeaderView(Context context) {
+ this(context, null);
+ }
+
+ public ContactDetailHeaderView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ContactDetailHeaderView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final LayoutInflater inflater =
+ (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.contact_detail_header_view, this);
+
+ mDisplayNameView = (TextView) findViewById(R.id.name);
+
+ mPhoneticNameView = (TextView) findViewById(R.id.phonetic_name);
+
+ mStarredView = (CheckBox)findViewById(R.id.star);
+ mStarredView.setOnClickListener(this);
+
+ mEditButton = (ImageButton) findViewById(R.id.edit);
+ mEditButton.setOnClickListener(this);
+
+ mPhotoView = (QuickContactBadge) findViewById(R.id.photo);
+
+ mPresenceView = (ImageView) findViewById(R.id.presence);
+
+ mStatusView = (TextView)findViewById(R.id.status);
+ mStatusAttributionView = (TextView)findViewById(R.id.status_date);
+ }
+
+ /**
+ * Loads the data from the Loader-Result. This is the only function that has to be called
+ * from the outside to fully setup the View
+ */
+ public void loadData(ContactLoader.Result contactData) {
+ mContactUri = contactData.getLookupUri();
+ mPhotoView.assignContactUri(contactData.getLookupUri());
+
+ setDisplayName(contactData.getDisplayName(), contactData.getPhoneticName());
+ setPhoto(findPhoto(contactData));
+ setStared(contactData.getStarred());
+ setPresence(contactData.getPresence());
+ setStatus(
+ contactData.getStatus(), contactData.getStatusTimestamp(),
+ contactData.getStatusLabel(), contactData.getStatusResPackage());
+ }
+
+ /**
+ * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If
+ * not found, returns null
+ */
+ private Bitmap findPhoto(ContactLoader.Result contactData) {
+ final long photoId = contactData.getPhotoId();
+
+ for (Entity entity : contactData.getEntities()) {
+ for (NamedContentValues subValue : entity.getSubValues()) {
+ final ContentValues entryValues = subValue.values;
+ final long dataId = entryValues.getAsLong(Data._ID);
+ final String mimeType = entryValues.getAsString(Data.MIMETYPE);
+
+ if (dataId == photoId) {
+ // Correct Data Id but incorrect MimeType? Don't load
+ if (!Photo.CONTENT_ITEM_TYPE.equals(mimeType)) return null;
+ final byte[] binaryData = entryValues.getAsByteArray(Photo.PHOTO);
+ if (binaryData == null) return null;
+ return BitmapFactory.decodeByteArray(binaryData, 0, binaryData.length);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Set the given {@link Listener} to handle header events.
+ */
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ private void performPhotoClick() {
+ if (mListener != null) {
+ mListener.onPhotoClick(mPhotoView);
+ }
+ }
+
+ private void performEditClick() {
+ if (mListener != null) {
+ mListener.onEditClicked();
+ }
+ }
+
+ private void performDisplayNameClick() {
+ if (mListener != null) {
+ mListener.onDisplayNameClick(mDisplayNameView);
+ }
+ }
+
+ /**
+ * Set the starred state of this header widget.
+ */
+ private void setStared(boolean starred) {
+ mStarredView.setChecked(starred);
+ }
+
+ /**
+ * Set the presence. If presence is null, it is hidden.
+ */
+ private void setPresence(Integer presence) {
+ if (presence == null) {
+ mPresenceView.setVisibility(View.GONE);
+ } else {
+ mPresenceView.setVisibility(View.VISIBLE);
+ mPresenceView.setImageResource(StatusUpdates.getPresenceIconResourceId(
+ presence.intValue()));
+ }
+ }
+
+ /**
+ * Set the photo to display in the header. If bitmap is null, the default placeholder
+ * image is shown
+ */
+ private void setPhoto(Bitmap bitmap) {
+ mPhotoView.setImageBitmap(bitmap == null ? loadPlaceholderPhoto() : bitmap);
+ }
+
+ /**
+ * Set the display name and phonetic name to show in the header.
+ */
+ private void setDisplayName(CharSequence displayName, CharSequence phoneticName) {
+ mDisplayNameView.setText(displayName);
+ if (TextUtils.isEmpty(phoneticName)) {
+ mPhoneticNameView.setVisibility(View.GONE);
+ } else {
+ mPhoneticNameView.setText(phoneticName);
+ mPhoneticNameView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Set the social snippet text to display in the header.
+ */
+ private void setSocialSnippet(CharSequence snippet) {
+ if (snippet == null) {
+ mStatusView.setVisibility(View.GONE);
+ mStatusAttributionView.setVisibility(View.GONE);
+ } else {
+ mStatusView.setText(snippet);
+ mStatusView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Set the status attribution text to display in the header.
+ */
+ private void setStatusAttribution(CharSequence attribution) {
+ if (attribution == null) {
+ mStatusAttributionView.setVisibility(View.GONE);
+ } else {
+ mStatusAttributionView.setText(attribution);
+ mStatusAttributionView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Set a list of specific MIME-types to exclude and not display. For
+ * example, this can be used to hide the {@link Contacts#CONTENT_ITEM_TYPE}
+ * profile icon.
+ */
+ public void setExcludeMimes(String[] excludeMimes) {
+ mPhotoView.setExcludeMimes(excludeMimes);
+ }
+
+ /**
+ * Set all the status values to display in the header.
+ * @param status The status of the contact. If this is either null or empty,
+ * the status is cleared and the other parameters are ignored.
+ * @param statusTimestamp The timestamp (retrieved via a call to
+ * {@link System#currentTimeMillis()}) of the last status update.
+ * This value can be null if it is not known.
+ * @param statusLabel The id of a resource string that specifies the current
+ * status. This value can be null if no Label should be used.
+ * @param statusResPackage The name of the resource package containing the resource string
+ * referenced in the parameter statusLabel.
+ */
+ private void setStatus(final String status, final Long statusTimestamp,
+ final Integer statusLabel, final String statusResPackage) {
+ if (TextUtils.isEmpty(status)) {
+ setSocialSnippet(null);
+ return;
+ }
+
+ setSocialSnippet(status);
+
+ final CharSequence timestampDisplayValue;
+
+ if (statusTimestamp != null) {
+ // Set the date/time field by mixing relative and absolute
+ // times.
+ int flags = DateUtils.FORMAT_ABBREV_RELATIVE;
+
+ timestampDisplayValue = DateUtils.getRelativeTimeSpanString(
+ statusTimestamp.longValue(), System.currentTimeMillis(),
+ DateUtils.MINUTE_IN_MILLIS, flags);
+ } else {
+ timestampDisplayValue = null;
+ }
+
+
+ String labelDisplayValue = null;
+
+ if (statusLabel != null) {
+ Resources resources;
+ if (TextUtils.isEmpty(statusResPackage)) {
+ resources = getResources();
+ } else {
+ PackageManager pm = getContext().getPackageManager();
+ try {
+ resources = pm.getResourcesForApplication(statusResPackage);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Contact status update resource package not found: "
+ + statusResPackage);
+ resources = null;
+ }
+ }
+
+ if (resources != null) {
+ try {
+ labelDisplayValue = resources.getString(statusLabel.intValue());
+ } catch (NotFoundException e) {
+ Log.w(TAG, "Contact status update resource not found: " + statusResPackage + "@"
+ + statusLabel.intValue());
+ }
+ }
+ }
+
+ final CharSequence attribution;
+ if (timestampDisplayValue != null && labelDisplayValue != null) {
+ attribution = getContext().getString(
+ R.string.contact_status_update_attribution_with_date,
+ timestampDisplayValue, labelDisplayValue);
+ } else if (timestampDisplayValue == null && labelDisplayValue != null) {
+ attribution = getContext().getString(
+ R.string.contact_status_update_attribution,
+ labelDisplayValue);
+ } else if (timestampDisplayValue != null) {
+ attribution = timestampDisplayValue;
+ } else {
+ attribution = null;
+ }
+ setStatusAttribution(attribution);
+ }
+
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.edit: {
+ performEditClick();
+ break;
+ }
+ case R.id.star: {
+ // Toggle "starred" state
+ // Make sure there is a contact
+ if (mContactUri != null) {
+ // TODO: This should be done in the background
+ final ContentValues values = new ContentValues(1);
+ values.put(Contacts.STARRED, mStarredView.isChecked());
+ mContext.getContentResolver().update(mContactUri, values, null, null);
+ }
+ break;
+ }
+ case R.id.photo: {
+ performPhotoClick();
+ break;
+ }
+ case R.id.name: {
+ performDisplayNameClick();
+ break;
+ }
+ }
+ }
+
+ private Bitmap loadPlaceholderPhoto() {
+ // Set the photo with a random "no contact" image
+ final long now = SystemClock.elapsedRealtime();
+ final int num = (int) now & 0xf;
+ final int resourceId;
+ if (num < 9) {
+ // Leaning in from right, common
+ resourceId = R.drawable.ic_contact_picture;
+ } else if (num < 14) {
+ // Leaning in from left uncommon
+ resourceId = R.drawable.ic_contact_picture_2;
+ } else {
+ // Coming in from the top, rare
+ resourceId = R.drawable.ic_contact_picture_3;
+ }
+
+ return BitmapFactory.decodeResource(mContext.getResources(), resourceId);
+ }
+}
diff --git a/src/com/android/contacts/views/editor/ContactEditorFragment.java b/src/com/android/contacts/views/editor/ContactEditorFragment.java
new file mode 100644
index 0000000..823c033
--- /dev/null
+++ b/src/com/android/contacts/views/editor/ContactEditorFragment.java
@@ -0,0 +1,591 @@
+/*
+ * 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.editor;
+
+import com.android.contacts.ContactOptionsActivity;
+import com.android.contacts.R;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Sources;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.ui.EditContactActivity;
+import com.android.contacts.views.ContactLoader;
+import com.android.contacts.views.editor.view.ViewTypes;
+import com.android.contacts.views.editor.viewModel.BaseViewModel;
+import com.android.contacts.views.editor.viewModel.EmailViewModel;
+import com.android.contacts.views.editor.viewModel.FooterViewModel;
+import com.android.contacts.views.editor.viewModel.ImViewModel;
+import com.android.contacts.views.editor.viewModel.NicknameViewModel;
+import com.android.contacts.views.editor.viewModel.NoteViewModel;
+import com.android.contacts.views.editor.viewModel.OrganizationViewModel;
+import com.android.contacts.views.editor.viewModel.PhoneViewModel;
+import com.android.contacts.views.editor.viewModel.PhotoViewModel;
+import com.android.contacts.views.editor.viewModel.StructuredNameViewModel;
+import com.android.contacts.views.editor.viewModel.StructuredPostalViewModel;
+import com.android.contacts.views.editor.viewModel.WebsiteViewModel;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.LoaderManagingFragment;
+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.Loader;
+import android.content.Entity.NamedContentValues;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+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.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.Log;
+import android.view.ContextMenu;
+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.BaseAdapter;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+
+public class ContactEditorFragment extends LoaderManagingFragment<ContactLoader.Result>
+ implements OnCreateContextMenuListener {
+ private static final String TAG = "ContactEditorFragment";
+
+ private static final String BUNDLE_RAW_CONTACT_ID = "rawContactId";
+
+ private static final int LOADER_DETAILS = 1;
+
+ private Context mContext;
+ private Uri mLookupUri;
+ private Listener mListener;
+
+ private boolean mIsInitialized;
+
+ private ContactLoader.Result mContactData;
+ private ContactEditorHeaderView mHeaderView;
+ private LinearLayout mFieldContainer;
+
+ private int mReadOnlySourcesCnt;
+ private int mWritableSourcesCnt;
+ private boolean mAllRestricted;
+
+ /**
+ * A list of RawContacts included in this Contact.
+ */
+ private ArrayList<DisplayRawContact> mRawContacts = new ArrayList<DisplayRawContact>();
+
+ private LayoutInflater mInflater;
+
+ public ContactEditorFragment() {
+ // Explicit constructor for inflation
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mContext = activity;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ final View view = inflater.inflate(R.layout.contact_editor_fragment, container, false);
+
+ setHasOptionsMenu(true);
+
+ mInflater = inflater;
+
+ mHeaderView = (ContactEditorHeaderView) view.findViewById(R.id.contact_header_widget);
+
+ mFieldContainer = (LinearLayout) view.findViewById(R.id.field_container);
+ mFieldContainer.setOnCreateContextMenuListener(this);
+ mFieldContainer.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+
+ return view;
+ }
+
+ public void setListener(Listener value) {
+ mListener = value;
+ }
+
+ public void loadUri(Uri lookupUri) {
+ mLookupUri = lookupUri;
+ if (mIsInitialized) startLoading(LOADER_DETAILS, null);
+ }
+
+ @Override
+ protected void onInitializeLoaders() {
+ mIsInitialized = true;
+ if (mLookupUri != null) startLoading(LOADER_DETAILS, null);
+ }
+
+ @Override
+ protected Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_DETAILS: {
+ return new ContactLoader(mContext, mLookupUri);
+ }
+ default: {
+ Log.wtf(TAG, "Unknown ID in onCreateLoader: " + id);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onLoadFinished(Loader<ContactLoader.Result> loader,
+ ContactLoader.Result data) {
+ final int id = loader.getId();
+ switch (id) {
+ case LOADER_DETAILS:
+ if (data == ContactLoader.Result.NOT_FOUND) {
+ // Item has been deleted
+ Log.i(TAG, "No contact found. Closing activity");
+ mListener.onContactNotFound();
+ return;
+ }
+ if (data == ContactLoader.Result.ERROR) {
+ // Item has been deleted
+ Log.i(TAG, "Error fetching contact. Closing activity");
+ mListener.onError();
+ return;
+ }
+ mContactData = data;
+ bindData();
+ break;
+ default: {
+ Log.wtf(TAG, "Unknown ID in onLoadFinished: " + id);
+ }
+ }
+ }
+
+ private void bindData() {
+ // Build up the contact entries
+ buildEntries();
+
+ mHeaderView.setMergeInfo(mRawContacts.size());
+
+ createFieldViews();
+ }
+
+ /**
+ * Build up the entries to display on the screen.
+ */
+ private final void buildEntries() {
+ // Clear out the old entries
+ mRawContacts.clear();
+
+ mReadOnlySourcesCnt = 0;
+ mWritableSourcesCnt = 0;
+ mAllRestricted = true;
+
+ // TODO: This should be done in the background thread
+ 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 String accountName = entValues.getAsString(RawContacts.ACCOUNT_NAME);
+ final long rawContactId = entValues.getAsLong(RawContacts._ID);
+ final String rawContactUriString = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+ rawContactId).toString();
+
+ // Mark when this contact has any unrestricted components
+ final boolean isRestricted = entValues.getAsInteger(RawContacts.IS_RESTRICTED) != 0;
+ if (!isRestricted) mAllRestricted = false;
+
+ final ContactsSource contactsSource = sources.getInflatedSource(accountType,
+ ContactsSource.LEVEL_SUMMARY);
+ final boolean writable = contactsSource == null || !contactsSource.readOnly;
+ if (writable) {
+ mWritableSourcesCnt += 1;
+ } else {
+ mReadOnlySourcesCnt += 1;
+ }
+
+ final DisplayRawContact rawContact = new DisplayRawContact(mContext, contactsSource,
+ accountName, rawContactId, writable, mRawContactFooterListener);
+ mRawContacts.add(rawContact);
+
+ 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;
+
+ // TODO: This surely can be written more nicely. Think about a factory once
+ // all editors are done
+ if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final StructuredNameViewModel itemEditor =
+ StructuredNameViewModel.createForExisting(mContext, rawContact, dataId,
+ entryValues, kind.titleRes);
+ rawContact.getFields().add(itemEditor);
+ continue;
+ }
+
+ if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final StructuredPostalViewModel itemEditor =
+ StructuredPostalViewModel.createForExisting(mContext, rawContact,
+ dataId, entryValues, kind.titleRes);
+ rawContact.getFields().add(itemEditor);
+ continue;
+ }
+
+ if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final PhoneViewModel itemEditor = PhoneViewModel.createForExisting(mContext,
+ rawContact, dataId, entryValues, kind.titleRes);
+ rawContact.getFields().add(itemEditor);
+ continue;
+ }
+
+ if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final EmailViewModel itemEditor = EmailViewModel.createForExisting(mContext,
+ rawContact, dataId, entryValues, kind.titleRes);
+ rawContact.getFields().add(itemEditor);
+ continue;
+ }
+
+ if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final ImViewModel itemEditor = ImViewModel.createForExisting(mContext,
+ rawContact, dataId, entryValues, kind.titleRes);
+ rawContact.getFields().add(itemEditor);
+ continue;
+ }
+
+ if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final NicknameViewModel itemEditor = NicknameViewModel.createForExisting(
+ mContext, rawContact, dataId, entryValues, kind.titleRes);
+ rawContact.getFields().add(itemEditor);
+ continue;
+ }
+
+ if (Note.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final NoteViewModel itemEditor = NoteViewModel.createForExisting(mContext,
+ rawContact, dataId, entryValues, kind.titleRes);
+ rawContact.getFields().add(itemEditor);
+ continue;
+ }
+
+ if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final WebsiteViewModel itemEditor = WebsiteViewModel.createForExisting(
+ mContext, rawContact, dataId, entryValues, kind.titleRes);
+ rawContact.getFields().add(itemEditor);
+ continue;
+ }
+
+ if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final OrganizationViewModel itemEditor =
+ OrganizationViewModel.createForExisting(mContext, rawContact, dataId,
+ entryValues, kind.titleRes);
+ rawContact.getFields().add(itemEditor);
+ continue;
+ }
+
+ if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final PhotoViewModel itemEditor =
+ PhotoViewModel.createForExisting(mContext, rawContact, dataId,
+ entryValues);
+ rawContact.getFields().add(itemEditor);
+ continue;
+ }
+ }
+ }
+ }
+
+ private void createFieldViews() {
+ mFieldContainer.removeAllViews();
+
+ for (int i = 0; i < mRawContacts.size(); i++) {
+ final DisplayRawContact rawContact = mRawContacts.get(i);
+ // Header
+ mFieldContainer.addView(
+ rawContact.getHeader().getView(mInflater, mFieldContainer));
+
+ // Data items
+ final ArrayList<BaseViewModel> fields = rawContact.getFields();
+ for (int j = 0; j < fields.size(); j++) {
+ final BaseViewModel field = fields.get(j);
+ mFieldContainer.addView(field.getView(mInflater, mFieldContainer));
+ }
+
+ // Footer
+ mFieldContainer.addView(
+ rawContact.getFooter().getView(mInflater, mFieldContainer));
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
+ inflater.inflate(R.menu.view, menu);
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ // TODO: Prepare options
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_edit: {
+ // TODO: This is temporary code to invoke the old editor. We can get rid of this
+ // later
+ final Intent intent = new Intent();
+ intent.setClass(mContext, EditContactActivity.class);
+ final long rawContactId = mRawContacts.get(0).getId();
+ final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+ rawContactId);
+ intent.setAction(Intent.ACTION_EDIT);
+ intent.setData(rawContactUri);
+ startActivity(intent);
+ return true;
+ }
+ 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() {
+ final int dialogId;
+ if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
+ dialogId = R.id.detail_dialog_confirm_readonly_delete;
+ } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
+ dialogId = R.id.detail_dialog_confirm_readonly_hide;
+ } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
+ dialogId = R.id.detail_dialog_confirm_multiple_delete;
+ } else {
+ dialogId = R.id.detail_dialog_confirm_delete;
+ }
+ if (mListener != null) mListener.onDialogRequested(dialogId, null);
+ }
+
+ // This was the ListView based code to expand/collapse sections.
+// public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+// if (mListener == null) return;
+// final BaseViewModel baseEntry = mAdapter.getEntry(position);
+// if (baseEntry == null) return;
+//
+// if (baseEntry instanceof HeaderViewModel) {
+// // Toggle rawcontact visibility
+// final HeaderViewModel entry = (HeaderViewModel) baseEntry;
+// entry.setCollapsed(!entry.isCollapsed());
+// mAdapter.notifyDataSetChanged();
+// }
+// }
+
+ private final DialogInterface.OnClickListener mDeleteListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mContext.getContentResolver().delete(mContactData.getLookupUri(), null, null);
+ }
+ };
+
+ public Dialog onCreateDialog(int id, Bundle bundle) {
+ // TODO The delete dialogs mirror the functionality from the Contact-Detail-Fragment.
+ // Consider whether we can extract common logic here
+ // TODO The actual removal is not in a worker thread currently
+ switch (id) {
+ case R.id.detail_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 R.id.detail_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 R.id.detail_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 R.id.detail_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();
+ }
+ case R.id.edit_dialog_add_information: {
+ final long rawContactId = bundle.getLong(BUNDLE_RAW_CONTACT_ID);
+ final DisplayRawContact rawContact = findRawContactById(rawContactId);
+ if (rawContact == null) return null;
+ final ContactsSource source = rawContact.getSource();
+
+ final ArrayList<DataKind> originalDataKinds = source.getSortedDataKinds();
+ // We should not modify the result returned from getSortedDataKinds but
+ // we have to filter some items out. Therefore we copy items into a new ArrayList
+ final ArrayList<DataKind> filteredDataKinds =
+ new ArrayList<DataKind>(originalDataKinds.size());
+ final ArrayList<String> items = new ArrayList<String>(filteredDataKinds.size());
+ for (DataKind dataKind : originalDataKinds) {
+ // TODO: Filter out fields that do not make sense in the current Context
+ // (Name, Photo, Notes etc)
+ if (dataKind.titleRes == -1) continue;
+ if (!dataKind.editable) continue;
+ final String title = mContext.getString(dataKind.titleRes);
+ items.add(title);
+ filteredDataKinds.add(dataKind);
+ }
+ final DialogInterface.OnClickListener itemClickListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // Create an Intent for the INSERT-Editor. Its data is null
+ // and the RawContact is identified in the Extras
+ final String rawContactUriString = ContentUris.withAppendedId(
+ RawContacts.CONTENT_URI, rawContactId).toString();
+ final DataKind dataKind = filteredDataKinds.get(which);
+ // TODO: Add new row
+// final Intent intent = new Intent();
+// intent.setType(dataKind.mimeType);
+// intent.setAction(Intent.ACTION_INSERT);
+// intent.putExtra(ContactFieldEditorActivity.BUNDLE_RAW_CONTACT_URI,
+// rawContactUriString);
+// if (mListener != null) mListener.onEditorRequested(intent);
+ }
+ };
+ return new AlertDialog.Builder(mContext)
+ .setItems(items.toArray(new String[0]), itemClickListener)
+ .create();
+ }
+ default:
+ return null;
+ }
+ }
+
+ private DisplayRawContact findRawContactById(long rawContactId) {
+ for (DisplayRawContact rawContact : mRawContacts) {
+ if (rawContact.getId() == rawContactId) return rawContact;
+ }
+ return null;
+ }
+
+ private FooterViewModel.Listener mRawContactFooterListener =
+ new FooterViewModel.Listener() {
+ public void onAddClicked(DisplayRawContact rawContact) {
+ // Create a bundle to show the Dialog
+ final Bundle bundle = new Bundle();
+ bundle.putLong(BUNDLE_RAW_CONTACT_ID, rawContact.getId());
+ if (mListener != null) {
+ mListener.onDialogRequested(R.id.edit_dialog_add_information, bundle);
+ }
+ }
+ public void onSeparateClicked(DisplayRawContact rawContact) {
+ }
+ public void onDeleteClicked(DisplayRawContact rawContact) {
+ }
+ };
+
+ public static interface Listener {
+ /**
+ * Contact was not found, so somehow close this fragment.
+ */
+ public void onContactNotFound();
+
+ /**
+ * There was an error loading the contact
+ */
+ public void onError();
+
+ /**
+ * User clicked a single item (e.g. mail) to edit it or is adding a new field
+ */
+ public void onEditorRequested(Intent intent);
+
+ /**
+ * Show a dialog using the globally unique id
+ */
+ public void onDialogRequested(int id, Bundle bundle);
+ }
+}
diff --git a/src/com/android/contacts/views/editor/ContactEditorHeaderView.java b/src/com/android/contacts/views/editor/ContactEditorHeaderView.java
new file mode 100644
index 0000000..18cbf72
--- /dev/null
+++ b/src/com/android/contacts/views/editor/ContactEditorHeaderView.java
@@ -0,0 +1,65 @@
+/*
+ * 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.views.editor;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+/**
+ * Header for displaying the title bar in the contact editor
+ */
+public class ContactEditorHeaderView extends FrameLayout {
+ private static final String TAG = "ContactEditorHeaderView";
+
+ private TextView mMergeInfo;
+
+ public ContactEditorHeaderView(Context context) {
+ this(context, null);
+ }
+
+ public ContactEditorHeaderView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ContactEditorHeaderView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final LayoutInflater inflater =
+ (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.contact_editor_header_view, this);
+
+ mMergeInfo = (TextView) findViewById(R.id.merge_info);
+
+ // Start with unmerged
+ setMergeInfo(1);
+ }
+
+ public void setMergeInfo(int count) {
+ if (count <= 1) {
+ mMergeInfo.setVisibility(GONE);
+ } else {
+ mMergeInfo.setVisibility(VISIBLE);
+ mMergeInfo.setText(
+ getResources().getQuantityString(R.plurals.merge_info, count, count));
+ }
+ }
+}
diff --git a/src/com/android/contacts/views/editor/DisplayRawContact.java b/src/com/android/contacts/views/editor/DisplayRawContact.java
new file mode 100644
index 0000000..0e6564b
--- /dev/null
+++ b/src/com/android/contacts/views/editor/DisplayRawContact.java
@@ -0,0 +1,74 @@
+/*
+ * 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.editor;
+
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.views.editor.viewModel.BaseViewModel;
+import com.android.contacts.views.editor.viewModel.FooterViewModel;
+import com.android.contacts.views.editor.viewModel.HeaderViewModel;
+
+import android.content.Context;
+
+import java.util.ArrayList;
+
+public class DisplayRawContact {
+ private final ContactsSource mSource;
+ private String mAccountName;
+ private final long mId;
+ private boolean mWritable;
+ private final HeaderViewModel mHeader;
+ private final FooterViewModel mFooter;
+ private final ArrayList<BaseViewModel> mFields = new ArrayList<BaseViewModel>();
+
+ public DisplayRawContact(Context context, ContactsSource source, String accountName, long id,
+ boolean writable, FooterViewModel.Listener footerListener) {
+ mSource = source;
+ mAccountName = accountName;
+ mId = id;
+ mWritable = writable;
+ mHeader = new HeaderViewModel(context, this);
+ mFooter = new FooterViewModel(context, this, footerListener);
+ }
+
+ public ContactsSource getSource() {
+ return mSource;
+ }
+
+ public String getAccountName() {
+ return mAccountName;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public boolean isWritable() {
+ return mWritable;
+ }
+
+ public ArrayList<BaseViewModel> getFields() {
+ return mFields;
+ }
+
+ public HeaderViewModel getHeader() {
+ return mHeader;
+ }
+
+ public FooterViewModel getFooter() {
+ return mFooter;
+ }
+}
diff --git a/src/com/android/contacts/views/editor/view/FieldAndTypeView.java b/src/com/android/contacts/views/editor/view/FieldAndTypeView.java
new file mode 100644
index 0000000..69e35b3
--- /dev/null
+++ b/src/com/android/contacts/views/editor/view/FieldAndTypeView.java
@@ -0,0 +1,98 @@
+/*
+ * 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.editor.view;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class FieldAndTypeView extends LinearLayout {
+ private TextView mCaptionTextView;
+ private EditText mFieldEditText;
+ private Button mTypeButton;
+ private Listener mListener;
+ private boolean mHasFocus;
+
+ public FieldAndTypeView(Context context) {
+ super(context);
+ }
+
+ public FieldAndTypeView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public FieldAndTypeView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public static FieldAndTypeView inflate(LayoutInflater inflater, ViewGroup parent,
+ boolean attachToRoot) {
+ return (FieldAndTypeView) inflater.inflate(R.layout.list_edit_item_field_and_type,
+ parent, attachToRoot);
+ }
+
+ public void setListener(Listener value) {
+ mListener = value;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mCaptionTextView = (TextView) findViewById(R.id.caption);
+ mFieldEditText = (EditText) findViewById(R.id.field);
+ mFieldEditText.setOnFocusChangeListener(mFieldEditTextFocusChangeListener);
+ mTypeButton = (Button) findViewById(R.id.type);
+ }
+
+ public void setLabelText(int resId) {
+ mCaptionTextView.setText(resId);
+ }
+
+ public void setFieldValue(CharSequence value) {
+ mFieldEditText.setText(value);
+ }
+
+ public CharSequence getFieldValue() {
+ return mFieldEditText.getText();
+ }
+
+ public void setTypeDisplayLabel(CharSequence type) {
+ mTypeButton.setText(type);
+ }
+
+ private OnFocusChangeListener mFieldEditTextFocusChangeListener = new OnFocusChangeListener() {
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (mHasFocus && !hasFocus && mListener != null) {
+ mListener.onFocusLost(FieldAndTypeView.this);
+ }
+ mHasFocus = hasFocus;
+ }
+ };
+
+ public interface Listener {
+ void onFocusLost(FieldAndTypeView view);
+ }
+}
diff --git a/src/com/android/contacts/views/editor/view/FooterView.java b/src/com/android/contacts/views/editor/view/FooterView.java
new file mode 100644
index 0000000..adc47cd
--- /dev/null
+++ b/src/com/android/contacts/views/editor/view/FooterView.java
@@ -0,0 +1,92 @@
+/*
+ * 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.editor.view;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+public class FooterView extends LinearLayout {
+ private Button mAddInformationButton;
+ private Button mSeparateButton;
+ private Button mDeleteButton;
+ private Listener mListener;
+
+ public FooterView(Context context) {
+ super(context);
+ }
+
+ public FooterView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public FooterView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public static FooterView inflate(LayoutInflater inflater, ViewGroup parent,
+ boolean attachToRoot) {
+ return (FooterView) inflater.inflate(R.layout.list_edit_item_footer, parent, attachToRoot);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mAddInformationButton = (Button) findViewById(R.id.add_information);
+ mAddInformationButton.setOnClickListener(mClickListener);
+
+ mSeparateButton = (Button) findViewById(R.id.separate);
+ mSeparateButton.setOnClickListener(mClickListener);
+
+ mDeleteButton = (Button) findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(mClickListener);
+ }
+
+ public void setListener(Listener value) {
+ mListener = value;
+ }
+
+ private OnClickListener mClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ if (mListener == null) return;
+ switch (v.getId()) {
+ case R.id.add_information:
+ mListener.onAddClicked();
+ break;
+ case R.id.separate:
+ mListener.onSeparateClicked();
+ break;
+ case R.id.delete:
+ mListener.onDeleteClicked();
+ break;
+ }
+ }
+ };
+
+ public static interface Listener {
+ void onAddClicked();
+ void onSeparateClicked();
+ void onDeleteClicked();
+ }
+}
diff --git a/src/com/android/contacts/views/editor/view/HeaderView.java b/src/com/android/contacts/views/editor/view/HeaderView.java
new file mode 100644
index 0000000..6822520
--- /dev/null
+++ b/src/com/android/contacts/views/editor/view/HeaderView.java
@@ -0,0 +1,66 @@
+/*
+ * 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.editor.view;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class HeaderView extends LinearLayout {
+ private ImageView mLogoImageView;
+ private TextView mCaptionTextView;
+
+ public HeaderView(Context context) {
+ super(context);
+ }
+
+ public HeaderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public HeaderView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public static HeaderView inflate(LayoutInflater inflater, ViewGroup parent,
+ boolean attachToRoot) {
+ return (HeaderView) inflater.inflate(R.layout.list_edit_item_header, parent, attachToRoot);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mLogoImageView = (ImageView) findViewById(R.id.logo);
+ mCaptionTextView = (TextView) findViewById(R.id.caption);
+ }
+
+ public void setCaptionText(String value) {
+ mCaptionTextView.setText(value);
+ }
+
+ public void setLogo(Drawable value) {
+ mLogoImageView.setImageDrawable(value);
+ }
+}
diff --git a/src/com/android/contacts/views/editor/view/OrganizationView.java b/src/com/android/contacts/views/editor/view/OrganizationView.java
new file mode 100644
index 0000000..53b400f
--- /dev/null
+++ b/src/com/android/contacts/views/editor/view/OrganizationView.java
@@ -0,0 +1,108 @@
+/*
+ * 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.editor.view;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class OrganizationView extends LinearLayout {
+ private TextView mCaptionTextView;
+ private EditText mCompanyFieldEditText;
+ private EditText mTitleFieldEditText;
+ private Button mTypeButton;
+ private Listener mListener;
+ private boolean mHasFocus;
+
+ public OrganizationView(Context context) {
+ super(context);
+ }
+
+ public OrganizationView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public OrganizationView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public static OrganizationView inflate(LayoutInflater inflater, ViewGroup parent,
+ boolean attachToRoot) {
+ return (OrganizationView) inflater.inflate(R.layout.list_edit_item_organization, parent,
+ attachToRoot);
+ }
+
+ public void setListener(Listener value) {
+ mListener = value;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mCaptionTextView = (TextView) findViewById(R.id.caption);
+ mCompanyFieldEditText = (EditText) findViewById(R.id.company_field);
+ mCompanyFieldEditText.setOnFocusChangeListener(mFieldEditTextFocusChangeListener);
+ mTitleFieldEditText = (EditText) findViewById(R.id.title_field);
+ mTitleFieldEditText.setOnFocusChangeListener(mFieldEditTextFocusChangeListener);
+ mTypeButton = (Button) findViewById(R.id.type);
+ }
+
+ public void setLabelText(int resId) {
+ mCaptionTextView.setText(resId);
+ }
+
+ public void setFieldValues(CharSequence company, CharSequence title) {
+ mCompanyFieldEditText.setText(company);
+ mTitleFieldEditText.setText(title);
+ }
+
+ public CharSequence getCompanyFieldValue() {
+ return mCompanyFieldEditText.getText();
+ }
+
+ public CharSequence getTitleFieldValue() {
+ return mTitleFieldEditText.getText();
+ }
+
+ public void setTypeDisplayLabel(CharSequence type) {
+ mTypeButton.setText(type);
+ }
+
+ private OnFocusChangeListener mFieldEditTextFocusChangeListener = new OnFocusChangeListener() {
+ public void onFocusChange(View v, boolean hasFocus) {
+ boolean anyHasFocus = mCompanyFieldEditText.hasFocus()
+ || mTitleFieldEditText.hasFocus();
+ if (mHasFocus && !anyHasFocus && mListener != null) {
+ mListener.onFocusLost(OrganizationView.this);
+ }
+ mHasFocus = anyHasFocus;
+ }
+ };
+
+ public interface Listener {
+ void onFocusLost(OrganizationView view);
+ }
+}
diff --git a/src/com/android/contacts/views/editor/view/PhotoView.java b/src/com/android/contacts/views/editor/view/PhotoView.java
new file mode 100644
index 0000000..d43e90e
--- /dev/null
+++ b/src/com/android/contacts/views/editor/view/PhotoView.java
@@ -0,0 +1,92 @@
+/*
+ * 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.editor.view;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+public class PhotoView extends LinearLayout {
+ private ImageView mPhotoImageView;
+ private ImageView mTakePhotoActionButton;
+ private ImageView mGalleryActionButton;
+ private Listener mListener;
+
+ public PhotoView(Context context) {
+ super(context);
+ }
+
+ public PhotoView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public PhotoView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public static PhotoView inflate(LayoutInflater inflater, ViewGroup parent,
+ boolean attachToRoot) {
+ return (PhotoView) inflater.inflate(R.layout.list_edit_item_photo, parent, attachToRoot);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mPhotoImageView = (ImageView) findViewById(R.id.photo);
+
+ mTakePhotoActionButton = (ImageView) findViewById(R.id.action_icon);
+ mTakePhotoActionButton.setOnClickListener(mClickListener);
+
+ mGalleryActionButton = (ImageView) findViewById(R.id.secondary_action_button);
+ mGalleryActionButton.setOnClickListener(mClickListener);
+ }
+
+ public void setListener(Listener value) {
+ mListener = value;
+ }
+
+ public void setPhoto(Bitmap value) {
+ mPhotoImageView.setImageBitmap(value);
+ }
+
+ private OnClickListener mClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ if (mListener == null) return;
+ switch (v.getId()) {
+ case R.id.action_icon:
+ mListener.onTakePhotoClicked();
+ break;
+ case R.id.secondary_action_button:
+ mListener.onChooseFromGalleryClicked();
+ break;
+ }
+ }
+ };
+
+ public static interface Listener {
+ void onTakePhotoClicked();
+ void onChooseFromGalleryClicked();
+ }
+}
diff --git a/src/com/android/contacts/views/editor/view/SimpleOrStructuredView.java b/src/com/android/contacts/views/editor/view/SimpleOrStructuredView.java
new file mode 100644
index 0000000..2bd0bd0
--- /dev/null
+++ b/src/com/android/contacts/views/editor/view/SimpleOrStructuredView.java
@@ -0,0 +1,104 @@
+/*
+ * 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.editor.view;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class SimpleOrStructuredView extends LinearLayout {
+ private TextView mCaptionTextView;
+ private EditText mFieldEditText;
+ private Button mStructuredEditorButton;
+ private Listener mListener;
+ private boolean mHasFocus;
+
+ public SimpleOrStructuredView(Context context) {
+ super(context);
+ }
+
+ public SimpleOrStructuredView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SimpleOrStructuredView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public static SimpleOrStructuredView inflate(LayoutInflater inflater, ViewGroup parent,
+ boolean attachToRoot) {
+ return (SimpleOrStructuredView) inflater.inflate(
+ R.layout.list_edit_item_simple_or_structured, parent, attachToRoot);
+ }
+
+ public void setListener(Listener value) {
+ mListener = value;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mCaptionTextView = (TextView) findViewById(R.id.caption);
+ mFieldEditText = (EditText) findViewById(R.id.field);
+ mFieldEditText.setOnFocusChangeListener(mFieldEditTextFocusChangeListener);
+ mStructuredEditorButton = (Button) findViewById(R.id.structuredEditorButton);
+ mStructuredEditorButton.setOnClickListener(mFullEditorClickListener);
+ }
+
+ public void setLabelText(int resId) {
+ mCaptionTextView.setText(resId);
+ }
+
+ public void setDisplayName(CharSequence value) {
+ mFieldEditText.setText(value);
+ }
+
+ public CharSequence getDisplayName() {
+ return mFieldEditText.getText();
+ }
+
+ private OnFocusChangeListener mFieldEditTextFocusChangeListener = new OnFocusChangeListener() {
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (mHasFocus && !hasFocus && mListener != null) {
+ mListener.onFocusLost(SimpleOrStructuredView.this);
+ }
+ mHasFocus = hasFocus;
+ }
+ };
+
+ private OnClickListener mFullEditorClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ if (mListener != null) {
+ mListener.onStructuredEditorRequested(SimpleOrStructuredView.this);
+ }
+ }
+ };
+
+ public interface Listener {
+ void onFocusLost(SimpleOrStructuredView view);
+ void onStructuredEditorRequested(SimpleOrStructuredView view);
+ }
+}
diff --git a/src/com/android/contacts/views/editor/view/SingleFieldView.java b/src/com/android/contacts/views/editor/view/SingleFieldView.java
new file mode 100644
index 0000000..2d4695e
--- /dev/null
+++ b/src/com/android/contacts/views/editor/view/SingleFieldView.java
@@ -0,0 +1,91 @@
+/*
+ * 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.editor.view;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class SingleFieldView extends LinearLayout {
+ private TextView mCaptionTextView;
+ private EditText mFieldEditText;
+ private Listener mListener;
+ private boolean mHasFocus;
+
+ public SingleFieldView(Context context) {
+ super(context);
+ }
+
+ public SingleFieldView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SingleFieldView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public static SingleFieldView inflate(LayoutInflater inflater, ViewGroup parent,
+ boolean attachToRoot) {
+ return (SingleFieldView) inflater.inflate(R.layout.list_edit_item_single_field,
+ parent, attachToRoot);
+ }
+
+ public void setListener(Listener value) {
+ mListener = value;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mCaptionTextView = (TextView) findViewById(R.id.caption);
+ mFieldEditText = (EditText) findViewById(R.id.field);
+ mFieldEditText.setOnFocusChangeListener(mFieldEditTextFocusChangeListener);
+ }
+
+ public void setLabelText(int resId) {
+ mCaptionTextView.setText(resId);
+ }
+
+ public void setFieldValue(CharSequence value) {
+ mFieldEditText.setText(value);
+ }
+
+ public CharSequence getFieldValue() {
+ return mFieldEditText.getText();
+ }
+
+ private OnFocusChangeListener mFieldEditTextFocusChangeListener = new OnFocusChangeListener() {
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (mHasFocus && !hasFocus && mListener != null) {
+ mListener.onFocusLost(SingleFieldView.this);
+ }
+ mHasFocus = hasFocus;
+ }
+ };
+
+ public interface Listener {
+ void onFocusLost(SingleFieldView view);
+ }
+}
diff --git a/src/com/android/contacts/views/editor/view/ViewTypes.java b/src/com/android/contacts/views/editor/view/ViewTypes.java
new file mode 100644
index 0000000..9795e40
--- /dev/null
+++ b/src/com/android/contacts/views/editor/view/ViewTypes.java
@@ -0,0 +1,29 @@
+/*
+ * 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.editor.view;
+
+public interface ViewTypes {
+ public static final int DATA = 0;
+ public static final int SINGLE_FIELD = 1;
+ public static final int FIELD_AND_TYPE = 2;
+ public static final int SIMPLE_OR_STRUCTURED = 3;
+ public static final int PHOTO = 4;
+ public static final int ORGANIZATION = 5;
+ public static final int RAW_CONTACT_HEADER = 6;
+ public static final int RAW_CONTACT_FOOTER = 7;
+ public static final int _COUNT = 8;
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/BaseViewModel.java b/src/com/android/contacts/views/editor/viewModel/BaseViewModel.java
new file mode 100644
index 0000000..8a2f78a
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/BaseViewModel.java
@@ -0,0 +1,47 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public abstract class BaseViewModel {
+ private final DisplayRawContact mRawContact;
+ private final Context mContext;
+
+ public BaseViewModel(Context context, DisplayRawContact rawContact) {
+ if (context == null) throw new IllegalArgumentException("context must not be null");
+ if (rawContact == null) throw new IllegalArgumentException("rawContact must not be null");
+ mContext = context;
+ mRawContact = rawContact;
+ }
+
+ public DisplayRawContact getRawContact() {
+ return mRawContact;
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public abstract int getEntryType();
+ public abstract View getView(LayoutInflater inflater, ViewGroup parent);
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/DataViewModel.java b/src/com/android/contacts/views/editor/viewModel/DataViewModel.java
new file mode 100644
index 0000000..23e98e4
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/DataViewModel.java
@@ -0,0 +1,101 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.ContactSaveService;
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.internal.util.ArrayUtils;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ContentProviderOperation.Builder;
+import android.net.Uri;
+import android.provider.ContactsContract.Data;
+
+import java.util.ArrayList;
+
+public abstract class DataViewModel extends BaseViewModel {
+ private final long mDataId;
+ private final ContentValues mContentValues;
+ private final String mMimeType;
+ private final Uri mDataUri;
+
+ protected DataViewModel(Context context, DisplayRawContact rawContact, long dataId,
+ ContentValues contentValues, String mimeType) {
+ super(context, rawContact);
+ mDataId = dataId;
+ mContentValues = contentValues;
+ mMimeType = mimeType;
+ mDataUri = ContentUris.withAppendedId(Data.CONTENT_URI, mDataId);
+ }
+
+ public long getDataId() {
+ return mDataId;
+ }
+
+ public Uri getDataUri() {
+ return mDataUri;
+ }
+
+ protected ContentValues getContentValues() {
+ return mContentValues;
+ }
+
+ public String getMimeType() {
+ return mMimeType;
+ }
+
+ /**
+ * Uncoditionally saves the current state to the database. No difference analysis is performed
+ */
+ public void saveData() {
+ final ContentResolver resolver = getContext().getContentResolver();
+
+ final ArrayList<ContentProviderOperation> operations =
+ new ArrayList<ContentProviderOperation>();
+
+ final Builder builder;
+ if (getDataUri() == null) {
+ // INSERT
+ builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+ builder.withValue(Data.MIMETYPE, getMimeType());
+ builder.withValue(Data.RAW_CONTACT_ID, getRawContact().getId());
+ writeToBuilder(builder, true);
+ } else {
+ // UPDATE
+ builder = ContentProviderOperation.newUpdate(getDataUri());
+ writeToBuilder(builder, false);
+ }
+ operations.add(builder.build());
+
+ // Tell the Service to save
+ // TODO: Handle the case where the data element has been removed in the background
+ final Intent serviceIntent = new Intent();
+ final ContentProviderOperation[] operationsArray =
+ operations.toArray(ArrayUtils.emptyArray(ContentProviderOperation.class));
+ serviceIntent.putExtra(ContactSaveService.EXTRA_OPERATIONS, operationsArray);
+ serviceIntent.setClass(getContext().getApplicationContext(), ContactSaveService.class);
+
+ getContext().startService(serviceIntent);
+ }
+
+ protected abstract void writeToBuilder(final Builder builder, boolean isInsert);
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/EmailViewModel.java b/src/com/android/contacts/views/editor/viewModel/EmailViewModel.java
new file mode 100644
index 0000000..bcc4397
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/EmailViewModel.java
@@ -0,0 +1,41 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+
+public class EmailViewModel extends FieldAndTypeViewModel {
+ private EmailViewModel(Context context, DisplayRawContact rawContact, long dataId,
+ ContentValues contentValues, int titleResId) {
+ super(context, rawContact, dataId, contentValues, Email.CONTENT_ITEM_TYPE, titleResId,
+ Email.ADDRESS, Email.TYPE, Email.LABEL);
+ }
+
+ public static EmailViewModel createForExisting(Context context, DisplayRawContact rawContact,
+ long dataId, ContentValues contentValues, int titleResId) {
+ return new EmailViewModel(context, rawContact, dataId, contentValues, titleResId);
+ }
+
+ @Override
+ protected CharSequence getTypeDisplayLabel() {
+ return Email.getTypeLabel(getContext().getResources(), getType(), getLabel());
+ }
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/FieldAndTypeViewModel.java b/src/com/android/contacts/views/editor/viewModel/FieldAndTypeViewModel.java
new file mode 100644
index 0000000..c4cc931
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/FieldAndTypeViewModel.java
@@ -0,0 +1,118 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.contacts.views.editor.view.FieldAndTypeView;
+import com.android.contacts.views.editor.view.ViewTypes;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContentProviderOperation.Builder;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public abstract class FieldAndTypeViewModel extends DataViewModel {
+ private static final String TAG = "FieldAndTypeViewModel";
+
+ private final int mLabelResId;
+ private final String mFieldColumn;
+ private final String mLabelColumn;
+ private final String mTypeColumn;
+
+ protected FieldAndTypeViewModel(Context context, DisplayRawContact rawContact,
+ long dataId, ContentValues contentValues, String mimeType, int labelResId,
+ String fieldColumn, String typeColumn, String labelColumn) {
+ super(context, rawContact, dataId, contentValues, mimeType);
+ mLabelResId = labelResId;
+
+ mFieldColumn = fieldColumn;
+ mTypeColumn = typeColumn;
+ mLabelColumn = labelColumn;
+ }
+
+ @Override
+ public int getEntryType() {
+ return ViewTypes.FIELD_AND_TYPE;
+ }
+
+ @Override
+ public FieldAndTypeView getView(LayoutInflater inflater, ViewGroup parent) {
+ final FieldAndTypeView result = FieldAndTypeView.inflate(inflater, parent, false);
+
+ result.setListener(mViewListener);
+ result.setLabelText(mLabelResId);
+ result.setFieldValue(getFieldValue());
+ result.setTypeDisplayLabel(getTypeDisplayLabel());
+
+ return result;
+ }
+
+ @Override
+ protected void writeToBuilder(Builder builder, boolean isInsert) {
+ builder.withValue(mFieldColumn, getFieldValue());
+ builder.withValue(mTypeColumn, getType());
+ builder.withValue(mLabelColumn, getLabel());
+ }
+
+ protected String getFieldValue() {
+ return getContentValues().getAsString(mFieldColumn);
+ }
+
+ protected void putFieldValue(String value) {
+ getContentValues().put(mFieldColumn, value);
+ }
+
+ protected int getType() {
+ return getContentValues().getAsInteger(mTypeColumn).intValue();
+ }
+
+ protected void putType(int value) {
+ getContentValues().put(mTypeColumn, value);
+ }
+
+ protected String getLabel() {
+ return getContentValues().getAsString(mLabelColumn);
+ }
+
+ protected void putLabel(String value) {
+ getContentValues().put(mLabelColumn, value);
+ }
+
+ protected abstract CharSequence getTypeDisplayLabel();
+
+ private FieldAndTypeView.Listener mViewListener = new FieldAndTypeView.Listener() {
+ public void onFocusLost(FieldAndTypeView view) {
+ Log.v(TAG, "Received FocusLost. Checking for changes");
+ boolean hasChanged = false;
+
+ final String oldValue = getFieldValue();
+ final String newValue = view.getFieldValue().toString();
+ if (!TextUtils.equals(oldValue, newValue)) {
+ putFieldValue(newValue);
+ hasChanged = true;
+ }
+ if (hasChanged) {
+ Log.v(TAG, "Found changes. Updating DB");
+ saveData();
+ }
+ }
+ };
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/FooterViewModel.java b/src/com/android/contacts/views/editor/viewModel/FooterViewModel.java
new file mode 100644
index 0000000..28610ef
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/FooterViewModel.java
@@ -0,0 +1,69 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.contacts.views.editor.view.FooterView;
+import com.android.contacts.views.editor.view.ViewTypes;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class FooterViewModel extends BaseViewModel {
+ private final Listener mListener;
+
+ public FooterViewModel(Context context, DisplayRawContact rawContact, Listener listener) {
+ super(context, rawContact);
+ if (listener == null) throw new IllegalArgumentException("listener must not be null");
+ mListener = listener;
+ }
+
+ @Override
+ public int getEntryType() {
+ return ViewTypes.RAW_CONTACT_FOOTER;
+ }
+
+ @Override
+ public View getView(LayoutInflater inflater, ViewGroup parent) {
+ final FooterView result = FooterView.inflate(inflater, parent, false);
+
+ result.setListener(mViewListener);
+ return result;
+ }
+
+ private FooterView.Listener mViewListener = new FooterView.Listener() {
+ public void onAddClicked() {
+ if (mListener != null) mListener.onAddClicked(getRawContact());
+ }
+
+ public void onSeparateClicked() {
+ if (mListener != null) mListener.onAddClicked(getRawContact());
+ }
+
+ public void onDeleteClicked() {
+ if (mListener != null) mListener.onAddClicked(getRawContact());
+ }
+ };
+
+ public interface Listener {
+ public void onAddClicked(DisplayRawContact rawContact);
+ public void onSeparateClicked(DisplayRawContact rawContact);
+ public void onDeleteClicked(DisplayRawContact rawContact);
+ }
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/HeaderViewModel.java b/src/com/android/contacts/views/editor/viewModel/HeaderViewModel.java
new file mode 100644
index 0000000..15b5226
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/HeaderViewModel.java
@@ -0,0 +1,74 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.R;
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.contacts.views.editor.view.HeaderView;
+import com.android.contacts.views.editor.view.ViewTypes;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class HeaderViewModel extends BaseViewModel {
+ private boolean mCollapsed;
+
+ public HeaderViewModel(Context context, DisplayRawContact rawContact) {
+ super(context, rawContact);
+ }
+
+ public boolean isCollapsed() {
+ return mCollapsed;
+ }
+
+ public void setCollapsed(boolean collapsed) {
+ mCollapsed = collapsed;
+ }
+
+ @Override
+ public int getEntryType() {
+ return ViewTypes.RAW_CONTACT_HEADER;
+ }
+
+ @Override
+ public View getView(LayoutInflater inflater, ViewGroup parent) {
+ final HeaderView result = HeaderView.inflate(inflater, parent, false);
+
+ CharSequence accountType = getRawContact().getSource().getDisplayLabel(getContext());
+ if (TextUtils.isEmpty(accountType)) {
+ accountType = getContext().getString(R.string.account_phone);
+ }
+ final String accountName = getRawContact().getAccountName();
+
+ final String accountTypeDisplay;
+ if (TextUtils.isEmpty(accountName)) {
+ accountTypeDisplay = getContext().getString(R.string.account_type_format,
+ accountType);
+ } else {
+ accountTypeDisplay = getContext().getString(R.string.account_type_and_name,
+ accountType, accountName);
+ }
+
+ result.setCaptionText(accountTypeDisplay);
+ result.setLogo(getRawContact().getSource().getDisplayIcon(getContext()));
+
+ return result;
+ }
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/ImViewModel.java b/src/com/android/contacts/views/editor/viewModel/ImViewModel.java
new file mode 100644
index 0000000..7ee3b21
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/ImViewModel.java
@@ -0,0 +1,58 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContentProviderOperation.Builder;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+
+/**
+ * Editor for Instant Messaging fields. The Type (HOME, WORK, OTHER, CUSTOM) is not shown but
+ * instead the same field is used for showing the Protocol (AIM, YAHOO etc). When
+ * creating new Im rows, the Type is set to OTHER
+ */
+public class ImViewModel extends FieldAndTypeViewModel {
+ private ImViewModel(Context context, DisplayRawContact rawContact, long dataId,
+ ContentValues contentValues, int titleResId) {
+ super(context, rawContact, dataId, contentValues, Im.CONTENT_ITEM_TYPE, titleResId, Im.DATA,
+ Im.PROTOCOL, Im.CUSTOM_PROTOCOL);
+ }
+
+ public static ImViewModel createForExisting(Context context, DisplayRawContact rawContact,
+ long dataId, ContentValues contentValues, int titleResId) {
+ return new ImViewModel(context, rawContact, dataId, contentValues, titleResId);
+ }
+
+ @Override
+ protected CharSequence getTypeDisplayLabel() {
+ return Im.getProtocolLabel(getContext().getResources(), getType(), getLabel());
+ }
+
+ @Override
+ protected void writeToBuilder(Builder builder, boolean isInsert) {
+ // The Type field is not exposed in the UI. Write OTHER for Insert but don't change it
+ // for updates
+ if (isInsert) {
+ builder.withValue(Im.TYPE, Im.TYPE_OTHER);
+ builder.withValue(Im.LABEL, "");
+ }
+ super.writeToBuilder(builder, isInsert);
+ }
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/NicknameViewModel.java b/src/com/android/contacts/views/editor/viewModel/NicknameViewModel.java
new file mode 100644
index 0000000..55c7976
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/NicknameViewModel.java
@@ -0,0 +1,36 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+
+public class NicknameViewModel extends SingleFieldViewModel {
+ private NicknameViewModel(Context context, DisplayRawContact rawContact, long dataId,
+ ContentValues contentValues, int titleResId) {
+ super(context, rawContact, dataId, contentValues, Nickname.CONTENT_ITEM_TYPE, titleResId,
+ Nickname.NAME);
+ }
+
+ public static NicknameViewModel createForExisting(Context context, DisplayRawContact rawContact,
+ long dataId, ContentValues contentValues, int titleResId) {
+ return new NicknameViewModel(context, rawContact, dataId, contentValues, titleResId);
+ }
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/NoteViewModel.java b/src/com/android/contacts/views/editor/viewModel/NoteViewModel.java
new file mode 100644
index 0000000..80e45d8
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/NoteViewModel.java
@@ -0,0 +1,36 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+
+public class NoteViewModel extends SingleFieldViewModel {
+ private NoteViewModel(Context context, DisplayRawContact rawContact, long dataId,
+ ContentValues contentValues, int titleResId) {
+ super(context, rawContact, dataId, contentValues, Note.CONTENT_ITEM_TYPE, titleResId,
+ Note.NOTE);
+ }
+
+ public static NoteViewModel createForExisting(Context context, DisplayRawContact rawContact,
+ long dataId, ContentValues contentValues, int titleResId) {
+ return new NoteViewModel(context, rawContact, dataId, contentValues, titleResId);
+ }
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/OrganizationViewModel.java b/src/com/android/contacts/views/editor/viewModel/OrganizationViewModel.java
new file mode 100644
index 0000000..1ccfb79
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/OrganizationViewModel.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.views.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.contacts.views.editor.view.OrganizationView;
+import com.android.contacts.views.editor.view.ViewTypes;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContentProviderOperation.Builder;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class OrganizationViewModel extends DataViewModel {
+ private static final String TAG = "OrganizationViewModel";
+
+ private final int mLabelResId;
+ private final String mCompanyFieldColumn;
+ private final String mTitleFieldColumn;
+ private final String mLabelColumn;
+ private final String mTypeColumn;
+
+ private OrganizationViewModel(Context context, DisplayRawContact rawContact,
+ long dataId, ContentValues contentValues, int labelResId) {
+ super(context, rawContact, dataId, contentValues, Organization.CONTENT_ITEM_TYPE);
+ mLabelResId = labelResId;
+
+ mCompanyFieldColumn = Organization.COMPANY;
+ mTitleFieldColumn = Organization.TITLE;
+ mTypeColumn = Organization.TYPE;
+ mLabelColumn = Organization.LABEL;
+ }
+
+ public static OrganizationViewModel createForExisting(Context context,
+ DisplayRawContact rawContact, long dataId, ContentValues contentValues,
+ int titleResId) {
+ return new OrganizationViewModel(context, rawContact, dataId, contentValues, titleResId);
+ }
+
+ @Override
+ public int getEntryType() {
+ return ViewTypes.ORGANIZATION;
+ }
+
+ @Override
+ public OrganizationView getView(LayoutInflater inflater, ViewGroup parent) {
+ final OrganizationView result = OrganizationView.inflate(inflater, parent, false);
+
+ result.setListener(mViewListener);
+ result.setLabelText(mLabelResId);
+ result.setFieldValues(getCompanyFieldValue(), getTitleFieldValue());
+ result.setTypeDisplayLabel(getTypeDisplayLabel());
+
+ return result;
+ }
+
+ @Override
+ protected void writeToBuilder(Builder builder, boolean isInsert) {
+ builder.withValue(mCompanyFieldColumn, getCompanyFieldValue());
+ builder.withValue(mTitleFieldColumn, getTitleFieldValue());
+ builder.withValue(mTypeColumn, getType());
+ builder.withValue(mLabelColumn, getLabel());
+ }
+
+ protected String getCompanyFieldValue() {
+ return getContentValues().getAsString(mCompanyFieldColumn);
+ }
+
+ protected String getTitleFieldValue() {
+ return getContentValues().getAsString(mTitleFieldColumn);
+ }
+
+ private void putCompanyFieldValue(String value) {
+ getContentValues().put(mCompanyFieldColumn, value);
+ }
+
+ private void putTitleFieldValue(String value) {
+ getContentValues().put(mTitleFieldColumn, value);
+ }
+
+ private int getType() {
+ return getContentValues().getAsInteger(mTypeColumn).intValue();
+ }
+
+ private void putType(int value) {
+ getContentValues().put(mTypeColumn, value);
+ }
+
+ private String getLabel() {
+ return getContentValues().getAsString(mLabelColumn);
+ }
+
+ private void putLabel(String value) {
+ getContentValues().put(mLabelColumn, value);
+ }
+
+ private CharSequence getTypeDisplayLabel() {
+ return Organization.getTypeLabel(getContext().getResources(), getType(), getLabel());
+ }
+
+ private OrganizationView.Listener mViewListener = new OrganizationView.Listener() {
+ public void onFocusLost(OrganizationView view) {
+ Log.v(TAG, "Received FocusLost. Checking for changes");
+ boolean hasChanged = false;
+
+ final String oldCompanyValue = getCompanyFieldValue();
+ final String newCompanyValue = view.getCompanyFieldValue().toString();
+ if (!TextUtils.equals(oldCompanyValue, newCompanyValue)) {
+ putCompanyFieldValue(newCompanyValue);
+ hasChanged = true;
+ }
+
+ final String oldTitleValue = getTitleFieldValue();
+ final String newTitleValue = view.getTitleFieldValue().toString();
+ if (!TextUtils.equals(oldTitleValue, newTitleValue)) {
+ putTitleFieldValue(newTitleValue);
+ hasChanged = true;
+ }
+ if (hasChanged) {
+ Log.v(TAG, "Found changes. Updating DB");
+ saveData();
+ }
+ }
+ };
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/PhoneViewModel.java b/src/com/android/contacts/views/editor/viewModel/PhoneViewModel.java
new file mode 100644
index 0000000..9ea033b
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/PhoneViewModel.java
@@ -0,0 +1,41 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+
+public class PhoneViewModel extends FieldAndTypeViewModel {
+ private PhoneViewModel(Context context, DisplayRawContact rawContact, long dataId,
+ ContentValues contentValues, int titleResId) {
+ super(context, rawContact, dataId, contentValues, Phone.CONTENT_ITEM_TYPE, titleResId,
+ Phone.NUMBER, Phone.TYPE, Phone.LABEL);
+ }
+
+ public static PhoneViewModel createForExisting(Context context, DisplayRawContact rawContact,
+ long dataId, ContentValues contentValues, int titleResId) {
+ return new PhoneViewModel(context, rawContact, dataId, contentValues, titleResId);
+ }
+
+ @Override
+ protected CharSequence getTypeDisplayLabel() {
+ return Phone.getTypeLabel(getContext().getResources(), getType(), getLabel());
+ }
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/PhotoViewModel.java b/src/com/android/contacts/views/editor/viewModel/PhotoViewModel.java
new file mode 100644
index 0000000..ae04933
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/PhotoViewModel.java
@@ -0,0 +1,70 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.contacts.views.editor.view.PhotoView;
+import com.android.contacts.views.editor.view.ViewTypes;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContentProviderOperation.Builder;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Editor for the contact photo.
+ */
+public class PhotoViewModel extends DataViewModel {
+ private PhotoViewModel(Context context, DisplayRawContact rawContact, long dataId,
+ ContentValues contentValues) {
+ super(context, rawContact, dataId, contentValues, Im.CONTENT_ITEM_TYPE);
+ }
+
+ public static PhotoViewModel createForExisting(Context context, DisplayRawContact rawContact,
+ long dataId, ContentValues contentValues) {
+ return new PhotoViewModel(context, rawContact, dataId, contentValues);
+ }
+
+ @Override
+ protected void writeToBuilder(Builder builder, boolean isInsert) {
+ // TODO
+ }
+
+ @Override
+ public int getEntryType() {
+ return ViewTypes.PHOTO;
+ }
+
+ @Override
+ public View getView(LayoutInflater inflater, ViewGroup parent) {
+ final PhotoView result = PhotoView.inflate(inflater, parent, false);
+
+ final byte[] binaryData = getContentValues().getAsByteArray(Photo.PHOTO);
+
+ final Bitmap bitmap = binaryData != null
+ ? BitmapFactory.decodeByteArray(binaryData, 0, binaryData.length)
+ : null;
+ result.setPhoto(bitmap);
+ return result;
+ }
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/SingleFieldViewModel.java b/src/com/android/contacts/views/editor/viewModel/SingleFieldViewModel.java
new file mode 100644
index 0000000..a5ef528
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/SingleFieldViewModel.java
@@ -0,0 +1,92 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.contacts.views.editor.view.SingleFieldView;
+import com.android.contacts.views.editor.view.ViewTypes;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContentProviderOperation.Builder;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public abstract class SingleFieldViewModel extends DataViewModel {
+ private static final String TAG = "SingleFieldViewModel";
+
+ private final int mLabelResId;
+ private final String mFieldColumn;
+
+ protected SingleFieldViewModel(Context context, DisplayRawContact rawContact,
+ long dataId, ContentValues contentValues, String mimeType, int labelResId,
+ String fieldColumn) {
+ super(context, rawContact, dataId, contentValues, mimeType);
+ mLabelResId = labelResId;
+ mFieldColumn = fieldColumn;
+ }
+
+ @Override
+ public int getEntryType() {
+ return ViewTypes.SINGLE_FIELD;
+ }
+
+ @Override
+ public SingleFieldView getView(LayoutInflater inflater, ViewGroup parent) {
+ final SingleFieldView result = SingleFieldView.inflate(inflater, parent, false);
+
+ result.setListener(mViewListener);
+ result.setLabelText(mLabelResId);
+ result.setFieldValue(getFieldValue());
+
+ return result;
+ }
+
+ @Override
+ protected void writeToBuilder(Builder builder, boolean isInsert) {
+ builder.withValue(mFieldColumn, getFieldValue());
+ }
+
+ protected String getFieldValue() {
+ return getContentValues().getAsString(mFieldColumn);
+ }
+
+ protected void putFieldValue(String value) {
+ getContentValues().put(mFieldColumn, value);
+ }
+
+ private SingleFieldView.Listener mViewListener = new SingleFieldView.Listener() {
+ public void onFocusLost(SingleFieldView view) {
+ Log.v(TAG, "Received FocusLost. Checking for changes");
+ boolean hasChanged = false;
+
+ final String oldValue = getFieldValue();
+ final String newValue = view.getFieldValue().toString();
+ if (!TextUtils.equals(oldValue, newValue)) {
+ putFieldValue(newValue);
+ hasChanged = true;
+ }
+ if (hasChanged) {
+ Log.v(TAG, "Found changes. Updating DB");
+ saveData();
+ }
+ }
+ };
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/StructuredNameViewModel.java b/src/com/android/contacts/views/editor/viewModel/StructuredNameViewModel.java
new file mode 100644
index 0000000..fb92b83
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/StructuredNameViewModel.java
@@ -0,0 +1,112 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.contacts.views.editor.view.SimpleOrStructuredView;
+import com.android.contacts.views.editor.view.ViewTypes;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContentProviderOperation.Builder;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Editor for the StructuredName. Handles both the structured representation as well the
+ * single field display
+ */
+public class StructuredNameViewModel extends DataViewModel {
+ protected static final String TAG = "StructuredNameViewModel";
+ private final int mLabelResId;
+
+ private StructuredNameViewModel(Context context, DisplayRawContact rawContact, long dataId,
+ ContentValues contentValues, int labelResId) {
+ super(context, rawContact, dataId, contentValues, StructuredName.CONTENT_ITEM_TYPE);
+ mLabelResId = labelResId;
+ }
+
+ public static StructuredNameViewModel createForExisting(Context context, DisplayRawContact rawContact,
+ long dataId, ContentValues contentValues, int labelResId) {
+ return new StructuredNameViewModel(context, rawContact, dataId, contentValues, labelResId);
+ }
+
+ @Override
+ protected void writeToBuilder(Builder builder, boolean isInsert) {
+ // TODO: Handle both structured and unstructured inputs.
+ // if (structuredEntered()) {
+ // } else {
+ builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName());
+ builder.withValue(StructuredName.GIVEN_NAME, null);
+ builder.withValue(StructuredName.FAMILY_NAME, null);
+ builder.withValue(StructuredName.PREFIX, null);
+ builder.withValue(StructuredName.MIDDLE_NAME, null);
+ builder.withValue(StructuredName.SUFFIX, null);
+ // }
+ }
+
+ @Override
+ public int getEntryType() {
+ return ViewTypes.SIMPLE_OR_STRUCTURED;
+ }
+
+ @Override
+ public View getView(LayoutInflater inflater, ViewGroup parent) {
+ final SimpleOrStructuredView result =
+ SimpleOrStructuredView.inflate(inflater, parent, false);
+
+ result.setListener(mViewListener);
+ result.setLabelText(mLabelResId);
+ result.setDisplayName(getDisplayName());
+
+ return result;
+ }
+
+ private String getDisplayName() {
+ return getContentValues().getAsString(StructuredName.DISPLAY_NAME);
+ }
+
+ private void putDisplayName(String value) {
+ getContentValues().put(StructuredName.DISPLAY_NAME, value);
+ }
+
+ private SimpleOrStructuredView.Listener mViewListener = new SimpleOrStructuredView.Listener() {
+ public void onFocusLost(SimpleOrStructuredView view) {
+ Log.v(TAG, "Received FocusLost. Checking for changes");
+ boolean hasChanged = false;
+
+ final String oldValue = getDisplayName();
+ final String newValue = view.getDisplayName().toString();
+ if (!TextUtils.equals(oldValue, newValue)) {
+ putDisplayName(newValue);
+ hasChanged = true;
+ }
+ if (hasChanged) {
+ Log.v(TAG, "Found changes. Updating DB");
+ saveData();
+ }
+ }
+
+ public void onStructuredEditorRequested(SimpleOrStructuredView view) {
+ // TODO
+ }
+ };
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/StructuredPostalViewModel.java b/src/com/android/contacts/views/editor/viewModel/StructuredPostalViewModel.java
new file mode 100644
index 0000000..ef46bdb
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/StructuredPostalViewModel.java
@@ -0,0 +1,121 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.contacts.views.editor.view.SimpleOrStructuredView;
+import com.android.contacts.views.editor.view.ViewTypes;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContentProviderOperation.Builder;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Editor for the StructuredPostal. Handles both the structured representation as well the
+ * single field display
+ */
+public class StructuredPostalViewModel extends DataViewModel {
+ protected static final String TAG = "StructuredPostalViewModel";
+ private final int mLabelResId;
+
+ private StructuredPostalViewModel(Context context, DisplayRawContact rawContact, long dataId,
+ ContentValues contentValues, int labelResId) {
+ super(context, rawContact, dataId, contentValues, StructuredPostal.CONTENT_ITEM_TYPE);
+ mLabelResId = labelResId;
+ }
+
+ public static StructuredPostalViewModel createForExisting(Context context,
+ DisplayRawContact rawContact, long dataId, ContentValues contentValues,
+ int labelResId) {
+ return new StructuredPostalViewModel(context, rawContact, dataId, contentValues,
+ labelResId);
+ }
+
+ @Override
+ protected void writeToBuilder(Builder builder, boolean isInsert) {
+ // TODO: Writing the non-structured field works pretty badly as we
+ // currently can't parse it properly. This is also a problem when an address it taken
+ // from Maps
+
+ // TODO: Handle both structured and unstructured inputs.
+
+ // if (structuredEntered()) {
+ // } else {
+ builder.withValue(StructuredPostal.FORMATTED_ADDRESS, getFormattedAddress());
+ builder.withValue(StructuredPostal.STREET, null);
+ builder.withValue(StructuredPostal.POBOX, null);
+ builder.withValue(StructuredPostal.NEIGHBORHOOD, null);
+ builder.withValue(StructuredPostal.CITY, null);
+ builder.withValue(StructuredPostal.REGION, null);
+ builder.withValue(StructuredPostal.POSTCODE, null);
+ builder.withValue(StructuredPostal.COUNTRY, null);
+ // }
+ }
+
+ @Override
+ public int getEntryType() {
+ return ViewTypes.SIMPLE_OR_STRUCTURED;
+ }
+
+ @Override
+ public View getView(LayoutInflater inflater, ViewGroup parent) {
+ final SimpleOrStructuredView result =
+ SimpleOrStructuredView.inflate(inflater, parent, false);
+
+ result.setListener(mViewListener);
+ result.setLabelText(mLabelResId);
+ result.setDisplayName(getFormattedAddress());
+
+ return result;
+ }
+
+ private String getFormattedAddress() {
+ return getContentValues().getAsString(StructuredPostal.FORMATTED_ADDRESS);
+ }
+
+ private void putDisplayName(String value) {
+ getContentValues().put(StructuredPostal.FORMATTED_ADDRESS, value);
+ }
+
+ private SimpleOrStructuredView.Listener mViewListener = new SimpleOrStructuredView.Listener() {
+ public void onFocusLost(SimpleOrStructuredView view) {
+ Log.v(TAG, "Received FocusLost. Checking for changes");
+ boolean hasChanged = false;
+
+ final String oldValue = getFormattedAddress();
+ final String newValue = view.getDisplayName().toString();
+ if (!TextUtils.equals(oldValue, newValue)) {
+ putDisplayName(newValue);
+ hasChanged = true;
+ }
+ if (hasChanged) {
+ Log.v(TAG, "Found changes. Updating DB");
+ saveData();
+ }
+ }
+
+ public void onStructuredEditorRequested(SimpleOrStructuredView view) {
+ // TODO
+ }
+ };
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/WebsiteViewModel.java b/src/com/android/contacts/views/editor/viewModel/WebsiteViewModel.java
new file mode 100644
index 0000000..5aabb29
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/WebsiteViewModel.java
@@ -0,0 +1,36 @@
+/*
+ * 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.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+
+public class WebsiteViewModel extends SingleFieldViewModel {
+ private WebsiteViewModel(Context context, DisplayRawContact rawContact, long dataId,
+ ContentValues contentValues, int titleResId) {
+ super(context, rawContact, dataId, contentValues, Website.CONTENT_ITEM_TYPE, titleResId,
+ Website.URL);
+ }
+
+ public static WebsiteViewModel createForExisting(Context context, DisplayRawContact rawContact,
+ long dataId, ContentValues contentValues, int titleResId) {
+ return new WebsiteViewModel(context, rawContact, dataId, contentValues, titleResId);
+ }
+}
diff --git a/src/com/android/contacts/widget/CompositeCursorAdapter.java b/src/com/android/contacts/widget/CompositeCursorAdapter.java
new file mode 100644
index 0000000..147ed42
--- /dev/null
+++ b/src/com/android/contacts/widget/CompositeCursorAdapter.java
@@ -0,0 +1,491 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * A general purpose adapter that is composed of multiple cursors. It just
+ * appends them in the order they are added.
+ */
+public abstract class CompositeCursorAdapter extends BaseAdapter {
+
+ private static final int INITIAL_CAPACITY = 2;
+
+ public static class Partition {
+ boolean showIfEmpty;
+ boolean hasHeader;
+
+ Cursor cursor;
+ int idColumnIndex;
+ int count;
+
+ public Partition(boolean showIfEmpty, boolean hasHeader) {
+ this.showIfEmpty = showIfEmpty;
+ this.hasHeader = hasHeader;
+ }
+
+ /**
+ * True if the directory should be shown even if no contacts are found.
+ */
+ public boolean getShowIfEmpty() {
+ return showIfEmpty;
+ }
+
+ public boolean getHasHeader() {
+ return hasHeader;
+ }
+ }
+
+ private final Context mContext;
+ private Partition[] mPartitions;
+ private int mSize = 0;
+ private int mCount = 0;
+ private boolean mCacheValid = true;
+
+ public CompositeCursorAdapter(Context context) {
+ this(context, INITIAL_CAPACITY);
+ }
+
+ public CompositeCursorAdapter(Context context, int initialCapacity) {
+ mContext = context;
+ mPartitions = new Partition[INITIAL_CAPACITY];
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Registers a partition. The cursor for that partition can be set later.
+ * Partitions should be added in the order they are supposed to appear in the
+ * list.
+ */
+ public void addPartition(boolean showIfEmpty, boolean hasHeader) {
+ addPartition(new Partition(showIfEmpty, hasHeader));
+ }
+
+ public void addPartition(Partition partition) {
+ if (mSize >= mPartitions.length) {
+ int newCapacity = mSize + 2;
+ Partition[] newAdapters = new Partition[newCapacity];
+ System.arraycopy(mPartitions, 0, newAdapters, 0, mSize);
+ mPartitions = newAdapters;
+ }
+ mPartitions[mSize++] = partition;
+ invalidate();
+ notifyDataSetChanged();
+ }
+
+ public void removePartition(int partitionIndex) {
+ Cursor cursor = mPartitions[partitionIndex].cursor;
+ if (cursor != null && !cursor.isClosed()) {
+ cursor.close();
+ }
+
+ System.arraycopy(mPartitions, partitionIndex + 1, mPartitions, partitionIndex, mSize - 1);
+ mSize--;
+ invalidate();
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Removes cursors for all partitions, closing them as necessary.
+ */
+ public void clearPartitions() {
+ for (int i = 0; i < mSize; i++) {
+ Cursor cursor = mPartitions[i].cursor;
+ if (cursor != null && !cursor.isClosed()) {
+ cursor.close();
+ }
+ mPartitions[i].cursor = null;
+ }
+ invalidate();
+ notifyDataSetChanged();
+ }
+
+ public void setHasHeader(int partitionIndex, boolean flag) {
+ mPartitions[partitionIndex].hasHeader = flag;
+ invalidate();
+ }
+
+ public void setShowIfEmpty(int partitionIndex, boolean flag) {
+ mPartitions[partitionIndex].showIfEmpty = flag;
+ invalidate();
+ }
+
+ public Partition getPartition(int partitionIndex) {
+ if (partitionIndex >= mSize) {
+ throw new ArrayIndexOutOfBoundsException(partitionIndex);
+ }
+ return mPartitions[partitionIndex];
+ }
+
+ protected void invalidate() {
+ mCacheValid = false;
+ }
+
+ public int getPartitionCount() {
+ return mSize;
+ }
+
+ protected void ensureCacheValid() {
+ if (mCacheValid) {
+ return;
+ }
+
+ mCount = 0;
+ for (int i = 0; i < mSize; i++) {
+ Cursor cursor = mPartitions[i].cursor;
+ int count = cursor != null ? cursor.getCount() : 0;
+ if (mPartitions[i].hasHeader) {
+ if (count != 0 || mPartitions[i].showIfEmpty) {
+ count++;
+ }
+ }
+ mPartitions[i].count = count;
+ mCount += count;
+ }
+
+ mCacheValid = true;
+ }
+
+ /**
+ * Returns true if the specified partition was configured to have a header.
+ */
+ public boolean hasHeader(int partition) {
+ return mPartitions[partition].hasHeader;
+ }
+
+ /**
+ * Returns the total number of list items in all partitions.
+ */
+ public int getCount() {
+ ensureCacheValid();
+ return mCount;
+ }
+
+ /**
+ * Returns the cursor for the given partition
+ */
+ public Cursor getCursor(int partition) {
+ return mPartitions[partition].cursor;
+ }
+
+ /**
+ * Changes the cursor for an individual partition.
+ */
+ public void changeCursor(int partition, Cursor cursor) {
+ Cursor prevCursor = mPartitions[partition].cursor;
+ if (prevCursor != cursor) {
+ if (prevCursor != null && !prevCursor.isClosed()) {
+ prevCursor.close();
+ }
+ mPartitions[partition].cursor = cursor;
+ if (cursor != null) {
+ mPartitions[partition].idColumnIndex = cursor.getColumnIndex("_id");
+ }
+ invalidate();
+ notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Returns true if the specified partition has no cursor or an empty cursor.
+ */
+ public boolean isPartitionEmpty(int partition) {
+ Cursor cursor = mPartitions[partition].cursor;
+ return cursor == null || cursor.getCount() == 0;
+ }
+
+ /**
+ * Given a list position, returns the index of the corresponding partition.
+ */
+ public int getPartitionForPosition(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ return i;
+ }
+ start = end;
+ }
+ return -1;
+ }
+
+ /**
+ * Given a list position, return the offset of the corresponding item in its
+ * partition. The header, if any, will have offset -1.
+ */
+ public int getOffsetInPartition(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ int offset = position - start;
+ if (mPartitions[i].hasHeader) {
+ offset--;
+ }
+ return offset;
+ }
+ start = end;
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the first list position for the specified partition.
+ */
+ public int getPositionForPartition(int partition) {
+ ensureCacheValid();
+ int position = 0;
+ for (int i = 0; i < partition; i++) {
+ position += mPartitions[i].count;
+ }
+ return position;
+ }
+
+ /**
+ * Returns the overall number of view types across all partitions. An implementation
+ * of this method needs to ensure that the returned count is consistent with the
+ * values returned by {@link #getItemViewType(int,int)}.
+ */
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ /**
+ * Returns the view type for the list item at the specified position in the
+ * specified partition.
+ */
+ protected int getItemViewType(int partition, int position) {
+ return 0;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ int offset = position - start;
+ if (mPartitions[i].hasHeader && offset == 0) {
+ return IGNORE_ITEM_VIEW_TYPE;
+ }
+ return getItemViewType(i, position);
+ }
+ start = end;
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ int offset = position - start;
+ if (mPartitions[i].hasHeader) {
+ offset--;
+ }
+ View view;
+ if (offset == -1) {
+ view = getHeaderView(i, mPartitions[i].cursor, convertView, parent);
+ } else {
+ if (!mPartitions[i].cursor.moveToPosition(offset)) {
+ throw new IllegalStateException("Couldn't move cursor to position "
+ + offset);
+ }
+ view = getView(i, mPartitions[i].cursor, offset, convertView, parent);
+ }
+ if (view == null) {
+ throw new NullPointerException("View should not be null, partition: " + i
+ + " position: " + offset);
+ }
+ return view;
+ }
+ start = end;
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ /**
+ * Returns the header view for the specified partition, creating one if needed.
+ */
+ protected View getHeaderView(int partition, Cursor cursor, View convertView,
+ ViewGroup parent) {
+ View view = convertView != null
+ ? convertView
+ : newHeaderView(mContext, partition, cursor, parent);
+ bindHeaderView(view, partition, cursor);
+ return view;
+ }
+
+ /**
+ * Creates the header view for the specified partition.
+ */
+ protected View newHeaderView(Context context, int partition, Cursor cursor,
+ ViewGroup parent) {
+ return null;
+ }
+
+ /**
+ * Binds the header view for the specified partition.
+ */
+ protected void bindHeaderView(View view, int partition, Cursor cursor) {
+ }
+
+ /**
+ * Returns an item view for the specified partition, creating one if needed.
+ */
+ protected View getView(int partition, Cursor cursor, int position, View convertView,
+ ViewGroup parent) {
+ View view;
+ if (convertView != null) {
+ view = convertView;
+ } else {
+ view = newView(mContext, partition, cursor, position, parent);
+ }
+ bindView(view, partition, cursor, position);
+ return view;
+ }
+
+ /**
+ * Creates an item view for the specified partition and position. Position
+ * corresponds directly to the current cursor position.
+ */
+ protected abstract View newView(Context context, int partition, Cursor cursor, int position,
+ ViewGroup parent);
+
+ /**
+ * Binds an item view for the specified partition and position. Position
+ * corresponds directly to the current cursor position.
+ */
+ protected abstract void bindView(View v, int partition, Cursor cursor, int position);
+
+ /**
+ * Returns a pre-positioned cursor for the specified list position.
+ */
+ public Object getItem(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ int offset = position - start;
+ if (mPartitions[i].hasHeader) {
+ offset--;
+ }
+ if (offset == -1) {
+ return null;
+ }
+ Cursor cursor = mPartitions[i].cursor;
+ cursor.moveToPosition(offset);
+ return cursor;
+ }
+ start = end;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the item ID for the specified list position.
+ */
+ public long getItemId(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ int offset = position - start;
+ if (mPartitions[i].hasHeader) {
+ offset--;
+ }
+ if (offset == -1) {
+ return 0;
+ }
+ if (mPartitions[i].idColumnIndex == -1) {
+ return 0;
+ }
+
+ Cursor cursor = mPartitions[i].cursor;
+ if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) {
+ return 0;
+ }
+ return cursor.getLong(mPartitions[i].idColumnIndex);
+ }
+ start = end;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns false if any partition has a header.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ for (int i = 0; i < mSize; i++) {
+ if (mPartitions[i].hasHeader) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns true for all items except headers.
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ int offset = position - start;
+ if (mPartitions[i].hasHeader && offset == 0) {
+ return false;
+ } else {
+ return isEnabled(i, offset);
+ }
+ }
+ start = end;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the item at the specified offset of the specified
+ * partition is selectable and clickable.
+ */
+ protected boolean isEnabled(int partition, int position) {
+ return true;
+ }
+}
diff --git a/src/com/android/contacts/widget/CompositeListAdapter.java b/src/com/android/contacts/widget/CompositeListAdapter.java
new file mode 100644
index 0000000..bd0c8d6
--- /dev/null
+++ b/src/com/android/contacts/widget/CompositeListAdapter.java
@@ -0,0 +1,222 @@
+/*
+ * 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.widget;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+
+/**
+ * A general purpose adapter that is composed of multiple sub-adapters. It just
+ * appends them in the order they are added. It listens to changes from all
+ * sub-adapters and propagates them to its own listeners.
+ */
+public class CompositeListAdapter extends BaseAdapter {
+
+ private static final int INITIAL_CAPACITY = 2;
+
+ private ListAdapter[] mAdapters;
+ private int[] mCounts;
+ private int[] mViewTypeCounts;
+ private int mSize = 0;
+ private int mCount = 0;
+ private int mViewTypeCount = 0;
+ private boolean mAllItemsEnabled = true;
+ private boolean mCacheValid = true;
+
+ private DataSetObserver mDataSetObserver = new DataSetObserver() {
+
+ @Override
+ public void onChanged() {
+ invalidate();
+ notifyDataChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ invalidate();
+ notifyDataChanged();
+ }
+ };
+
+ public CompositeListAdapter() {
+ this(INITIAL_CAPACITY);
+ }
+
+ public CompositeListAdapter(int initialCapacity) {
+ mAdapters = new ListAdapter[INITIAL_CAPACITY];
+ mCounts = new int[INITIAL_CAPACITY];
+ mViewTypeCounts = new int[INITIAL_CAPACITY];
+ }
+
+ public void addAdapter(ListAdapter adapter) {
+ if (mSize >= mAdapters.length) {
+ int newCapacity = mSize + 2;
+ ListAdapter[] newAdapters = new ListAdapter[newCapacity];
+ System.arraycopy(mAdapters, 0, newAdapters, 0, mSize);
+ mAdapters = newAdapters;
+
+ int[] newCounts = new int[newCapacity];
+ System.arraycopy(mCounts, 0, newCounts, 0, mSize);
+ mCounts = newCounts;
+
+ int[] newViewTypeCounts = new int[newCapacity];
+ System.arraycopy(mViewTypeCounts, 0, newViewTypeCounts, 0, mSize);
+ mViewTypeCounts = newViewTypeCounts;
+ }
+
+ adapter.registerDataSetObserver(mDataSetObserver);
+
+ int count = adapter.getCount();
+ int viewTypeCount = adapter.getViewTypeCount();
+
+ mAdapters[mSize] = adapter;
+ mCounts[mSize] = count;
+ mCount += count;
+ mAllItemsEnabled &= adapter.areAllItemsEnabled();
+ mViewTypeCounts[mSize] = viewTypeCount;
+ mViewTypeCount += viewTypeCount;
+ mSize++;
+
+ notifyDataChanged();
+ }
+
+ protected void notifyDataChanged() {
+ if (getCount() > 0) {
+ notifyDataSetChanged();
+ } else {
+ notifyDataSetInvalidated();
+ }
+ }
+
+ protected void invalidate() {
+ mCacheValid = false;
+ }
+
+ protected void ensureCacheValid() {
+ if (mCacheValid) {
+ return;
+ }
+
+ mCount = 0;
+ mAllItemsEnabled = true;
+ mViewTypeCount = 0;
+ for (int i = 0; i < mSize; i++) {
+ int count = mAdapters[i].getCount();
+ int viewTypeCount = mAdapters[i].getViewTypeCount();
+ mCounts[i] = count;
+ mCount += count;
+ mAllItemsEnabled &= mAdapters[i].areAllItemsEnabled();
+ mViewTypeCount += viewTypeCount;
+ }
+
+ mCacheValid = true;
+ }
+
+ public int getCount() {
+ ensureCacheValid();
+ return mCount;
+ }
+
+ public Object getItem(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mCounts.length; i++) {
+ int end = start + mCounts[i];
+ if (position >= start && position < end) {
+ return mAdapters[i].getItem(position - start);
+ }
+ start = end;
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ public long getItemId(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mCounts.length; i++) {
+ int end = start + mCounts[i];
+ if (position >= start && position < end) {
+ return mAdapters[i].getItemId(position - start);
+ }
+ start = end;
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ ensureCacheValid();
+ return mViewTypeCount;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ ensureCacheValid();
+ int start = 0;
+ int viewTypeOffset = 0;
+ for (int i = 0; i < mCounts.length; i++) {
+ int end = start + mCounts[i];
+ if (position >= start && position < end) {
+ return viewTypeOffset + mAdapters[i].getItemViewType(position - start);
+ }
+ viewTypeOffset += mViewTypeCounts[i];
+ start = end;
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mCounts.length; i++) {
+ int end = start + mCounts[i];
+ if (position >= start && position < end) {
+ return mAdapters[i].getView(position - start, convertView, parent);
+ }
+ start = end;
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ ensureCacheValid();
+ return mAllItemsEnabled;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mCounts.length; i++) {
+ int end = start + mCounts[i];
+ if (position >= start && position < end) {
+ return mAdapters[i].areAllItemsEnabled()
+ || mAdapters[i].isEnabled(position - start);
+ }
+ start = end;
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+}
diff --git a/src/com/android/contacts/widget/ContextMenuAdapter.java b/src/com/android/contacts/widget/ContextMenuAdapter.java
new file mode 100644
index 0000000..660274a
--- /dev/null
+++ b/src/com/android/contacts/widget/ContextMenuAdapter.java
@@ -0,0 +1,30 @@
+/*
+ * 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.widget;
+
+import android.view.MenuItem;
+import android.view.View;
+
+/**
+ * An adapter for the contextual menu.
+ */
+public interface ContextMenuAdapter extends View.OnCreateContextMenuListener {
+
+ /**
+ * See {@link android.app.Activity#onContextItemSelected}.
+ */
+ boolean onContextItemSelected(MenuItem item);
+}
diff --git a/src/com/android/contacts/widget/IndexerListAdapter.java b/src/com/android/contacts/widget/IndexerListAdapter.java
new file mode 100644
index 0000000..c39e2f2
--- /dev/null
+++ b/src/com/android/contacts/widget/IndexerListAdapter.java
@@ -0,0 +1,175 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+
+/**
+ * A list adapter that supports section indexer and a pinned header.
+ */
+public abstract class IndexerListAdapter extends PinnedHeaderListAdapter implements SectionIndexer {
+
+ private final int mSectionHeaderTextViewId;
+ private final int mSectionHeaderLayoutResId;
+
+ protected Context mContext;
+ private SectionIndexer mIndexer;
+ private int mIndexedPartition = 0;
+ private boolean mSectionHeaderDisplayEnabled;
+ private View mHeader;
+ private TextView mTitleView;
+
+ /**
+ * Constructor.
+ *
+ * @param context
+ * @param sectionHeaderLayoutResourceId section header layout resource ID
+ * @param sectionHeaderTextViewId section header text view ID
+ */
+ public IndexerListAdapter(Context context, int sectionHeaderLayoutResourceId,
+ int sectionHeaderTextViewId) {
+ super(context);
+ mContext = context;
+ mSectionHeaderLayoutResId = sectionHeaderLayoutResourceId;
+ mSectionHeaderTextViewId = sectionHeaderTextViewId;
+ }
+
+ public boolean isSectionHeaderDisplayEnabled() {
+ return mSectionHeaderDisplayEnabled;
+ }
+
+ public void setSectionHeaderDisplayEnabled(boolean flag) {
+ this.mSectionHeaderDisplayEnabled = flag;
+ }
+
+ public int getIndexedPartition() {
+ return mIndexedPartition;
+ }
+
+ public void setIndexedPartition(int partition) {
+ this.mIndexedPartition = partition;
+ }
+
+ public SectionIndexer getIndexer() {
+ return mIndexer;
+ }
+
+ public void setIndexer(SectionIndexer indexer) {
+ mIndexer = indexer;
+ }
+
+ public Object[] getSections() {
+ if (mIndexer == null) {
+ return new String[] { " " };
+ } else {
+ return mIndexer.getSections();
+ }
+ }
+
+ /**
+ * @return relative position of the section in the indexed partition
+ */
+ public int getPositionForSection(int sectionIndex) {
+ if (mIndexer == null) {
+ return -1;
+ }
+
+ return mIndexer.getPositionForSection(sectionIndex);
+ }
+
+ /**
+ * @param position relative position in the indexed partition
+ */
+ public int getSectionForPosition(int position) {
+ if (mIndexer == null) {
+ return -1;
+ }
+
+ return mIndexer.getSectionForPosition(position);
+ }
+
+ @Override
+ public int getPinnedHeaderCount() {
+ if (isSectionHeaderDisplayEnabled()) {
+ return super.getPinnedHeaderCount() + 1;
+ } else {
+ return super.getPinnedHeaderCount();
+ }
+ }
+
+ @Override
+ public View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) {
+ if (isSectionHeaderDisplayEnabled() && viewIndex == getPinnedHeaderCount() - 1) {
+ if (mHeader == null) {
+ mHeader = LayoutInflater.from(mContext).
+ inflate(mSectionHeaderLayoutResId, parent, false);
+ mTitleView = (TextView)mHeader.findViewById(mSectionHeaderTextViewId);
+ }
+ return mHeader;
+ } else {
+ return super.getPinnedHeaderView(viewIndex, convertView, parent);
+ }
+ }
+
+ @Override
+ public void configurePinnedHeaders(PinnedHeaderListView listView) {
+ super.configurePinnedHeaders(listView);
+
+ if (!isSectionHeaderDisplayEnabled()) {
+ return;
+ }
+
+ int index = getPinnedHeaderCount() - 1;
+ if (mIndexer == null || getCount() == 0) {
+ listView.setHeaderInvisible(index, false);
+ } else {
+ int listPosition = listView.getPositionAt(listView.getTotalTopPinnedHeaderHeight());
+ int position = listPosition - listView.getHeaderViewsCount();
+
+ int section = -1;
+ int partition = getPartitionForPosition(position);
+ if (partition == mIndexedPartition) {
+ int offset = getOffsetInPartition(position);
+ if (offset != -1) {
+ section = getSectionForPosition(offset);
+ }
+ }
+
+ if (section == -1) {
+ listView.setHeaderInvisible(index, false);
+ } else {
+ String title = (String)mIndexer.getSections()[section];
+ mTitleView.setText(title);
+
+ // Compute the item position where the current partition begins
+ int partitionStart = getPositionForPartition(mIndexedPartition);
+ if (hasHeader(mIndexedPartition)) {
+ partitionStart++;
+ }
+
+ // Compute the item position where the next section begins
+ int nextSectionPosition = partitionStart + getPositionForSection(section + 1);
+ boolean isLastInSection = position == nextSectionPosition - 1;
+ listView.setFadingHeader(index, listPosition, isLastInSection);
+ }
+ }
+ }
+}
diff --git a/src/com/android/contacts/widget/PinnedHeaderListAdapter.java b/src/com/android/contacts/widget/PinnedHeaderListAdapter.java
new file mode 100644
index 0000000..e39bce8
--- /dev/null
+++ b/src/com/android/contacts/widget/PinnedHeaderListAdapter.java
@@ -0,0 +1,165 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A subclass of {@link CompositeCursorAdapter} that manages pinned partition headers.
+ */
+public abstract class PinnedHeaderListAdapter extends CompositeCursorAdapter
+ implements PinnedHeaderListView.PinnedHeaderAdapter {
+
+ public static final int PARTITION_HEADER_TYPE = 0;
+
+ private boolean mPinnedPartitionHeadersEnabled;
+ private boolean mHeaderVisibility[];
+
+ public PinnedHeaderListAdapter(Context context) {
+ super(context);
+ }
+
+ public PinnedHeaderListAdapter(Context context, int initialCapacity) {
+ super(context, initialCapacity);
+ }
+
+ public boolean getPinnedPartitionHeadersEnabled() {
+ return mPinnedPartitionHeadersEnabled;
+ }
+
+ public void setPinnedPartitionHeadersEnabled(boolean flag) {
+ this.mPinnedPartitionHeadersEnabled = flag;
+ }
+
+ public int getPinnedHeaderCount() {
+ if (mPinnedPartitionHeadersEnabled) {
+ return getPartitionCount();
+ } else {
+ return 0;
+ }
+ }
+
+ protected boolean isPinnedPartitionHeaderVisible(int partition) {
+ return mPinnedPartitionHeadersEnabled && hasHeader(partition)
+ && !isPartitionEmpty(partition);
+ }
+
+ /**
+ * The default implementation creates the same type of view as a normal
+ * partition header.
+ */
+ public View getPinnedHeaderView(int partition, View convertView, ViewGroup parent) {
+ if (hasHeader(partition)) {
+ View view = null;
+ if (convertView != null) {
+ Integer headerType = (Integer)convertView.getTag();
+ if (headerType != null && headerType == PARTITION_HEADER_TYPE) {
+ view = convertView;
+ }
+ }
+ if (view == null) {
+ view = newHeaderView(getContext(), partition, null, parent);
+ view.setTag(PARTITION_HEADER_TYPE);
+ view.setFocusable(false);
+ view.setEnabled(false);
+ }
+ bindHeaderView(view, partition, getCursor(partition));
+ return view;
+ } else {
+ return null;
+ }
+ }
+
+ public void configurePinnedHeaders(PinnedHeaderListView listView) {
+ if (!mPinnedPartitionHeadersEnabled) {
+ return;
+ }
+
+ int size = getPartitionCount();
+
+ // Cache visibility bits, because we will need them several times later on
+ if (mHeaderVisibility == null || mHeaderVisibility.length != size) {
+ mHeaderVisibility = new boolean[size];
+ }
+ for (int i = 0; i < size; i++) {
+ boolean visible = isPinnedPartitionHeaderVisible(i);
+ mHeaderVisibility[i] = visible;
+ if (!visible) {
+ listView.setHeaderInvisible(i, true);
+ }
+ }
+
+ int headerViewsCount = listView.getHeaderViewsCount();
+
+ // Starting at the top, find and pin headers for partitions preceding the visible one(s)
+ int maxTopHeader = -1;
+ int topHeaderHeight = 0;
+ for (int i = 0; i < size; i++) {
+ if (mHeaderVisibility[i]) {
+ int position = listView.getPositionAt(topHeaderHeight) - headerViewsCount;
+ int partition = getPartitionForPosition(position);
+ if (i > partition) {
+ break;
+ }
+
+ listView.setHeaderPinnedAtTop(i, topHeaderHeight, false);
+ topHeaderHeight += listView.getPinnedHeaderHeight(i);
+ maxTopHeader = i;
+ }
+ }
+
+ // Starting at the bottom, find and pin headers for partitions following the visible one(s)
+ int maxBottomHeader = size;
+ int bottomHeaderHeight = 0;
+ int listHeight = listView.getHeight();
+ for (int i = size; --i > maxTopHeader;) {
+ if (mHeaderVisibility[i]) {
+ int position = listView.getPositionAt(listHeight - bottomHeaderHeight)
+ - headerViewsCount;
+ if (position < 0) {
+ break;
+ }
+
+ int partition = getPartitionForPosition(position - 1);
+ if (partition == -1 || i <= partition) {
+ break;
+ }
+
+ int height = listView.getPinnedHeaderHeight(i);
+ bottomHeaderHeight += height;
+ // Animate the header only if the partition is completely invisible below
+ // the bottom of the view
+ int firstPositionForPartition = getPositionForPartition(i);
+ boolean animate = position < firstPositionForPartition;
+ listView.setHeaderPinnedAtBottom(i, listHeight - bottomHeaderHeight, animate);
+ maxBottomHeader = i;
+ }
+ }
+
+ // Headers in between the top-pinned and bottom-pinned should be hidden
+ for (int i = maxTopHeader + 1; i < maxBottomHeader; i++) {
+ if (mHeaderVisibility[i]) {
+ listView.setHeaderInvisible(i, isPartitionEmpty(i));
+ }
+ }
+ }
+
+ public int getScrollPositionForHeader(int viewIndex) {
+ return getPositionForPartition(viewIndex);
+ }
+}
diff --git a/src/com/android/contacts/widget/PinnedHeaderListDemoActivity.java b/src/com/android/contacts/widget/PinnedHeaderListDemoActivity.java
new file mode 100644
index 0000000..b553e3f
--- /dev/null
+++ b/src/com/android/contacts/widget/PinnedHeaderListDemoActivity.java
@@ -0,0 +1,142 @@
+/*
+ * 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.widget;
+
+import com.android.contacts.R;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/**
+ * An activity that demonstrates various use cases for the {@link PinnedHeaderListView}.
+ * If we decide to move PinnedHeaderListView to the framework, this class could go
+ * to API demos.
+ */
+public class PinnedHeaderListDemoActivity extends ListActivity {
+
+ public final static class TestPinnedHeaderListAdapter extends PinnedHeaderListAdapter {
+
+ public TestPinnedHeaderListAdapter(Context context) {
+ super(context);
+ setPinnedPartitionHeadersEnabled(true);
+ }
+
+ private String[] mHeaders;
+ private int mPinnedHeaderCount;
+
+ public void setHeaders(String[] headers) {
+ this.mHeaders = headers;
+ }
+
+ @Override
+ protected View newHeaderView(Context context, int partition, Cursor cursor,
+ ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ return inflater.inflate(R.layout.list_section, null);
+ }
+
+ @Override
+ protected void bindHeaderView(View view, int parition, Cursor cursor) {
+ TextView headerText = (TextView)view.findViewById(R.id.header_text);
+ headerText.setText(mHeaders[parition]);
+ }
+
+ @Override
+ protected View newView(Context context, int partition, Cursor cursor, int position,
+ ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ return inflater.inflate(android.R.layout.simple_list_item_1, null);
+ }
+
+ @Override
+ protected void bindView(View v, int partition, Cursor cursor, int position) {
+ TextView text = (TextView)v.findViewById(android.R.id.text1);
+ text.setText(cursor.getString(1));
+ }
+
+ @Override
+ public View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ View view = inflater.inflate(R.layout.list_section, parent, false);
+ view.setFocusable(false);
+ view.setEnabled(false);
+ bindHeaderView(view, viewIndex, null);
+ return view;
+ }
+
+ @Override
+ public int getPinnedHeaderCount() {
+ return mPinnedHeaderCount;
+ }
+ }
+
+ private Handler mHandler = new Handler();
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ setContentView(R.layout.pinned_header_list_demo);
+
+ final TestPinnedHeaderListAdapter adapter = new TestPinnedHeaderListAdapter(this);
+
+ Bundle extras = getIntent().getExtras();
+ int[] counts = extras.getIntArray("counts");
+ String[] names = extras.getStringArray("names");
+ boolean[] showIfEmpty = extras.getBooleanArray("showIfEmpty");
+ boolean[] hasHeader = extras.getBooleanArray("headers");
+ int[] delays = extras.getIntArray("delays");
+
+ if (counts == null || names == null || showIfEmpty == null || delays == null) {
+ throw new IllegalArgumentException("Missing required extras");
+ }
+
+ adapter.setHeaders(names);
+ for (int i = 0; i < counts.length; i++) {
+ adapter.addPartition(showIfEmpty[i], names[i] != null);
+ adapter.mPinnedHeaderCount = names.length;
+ }
+ setListAdapter(adapter);
+ for (int i = 0; i < counts.length; i++) {
+ final int sectionId = i;
+ final Cursor cursor = makeCursor(names[i], counts[i]);
+ mHandler.postDelayed(new Runnable() {
+
+ public void run() {
+ adapter.changeCursor(sectionId, cursor);
+
+ }
+ }, delays[i]);
+ }
+ }
+
+ private Cursor makeCursor(String name, int count) {
+ MatrixCursor cursor = new MatrixCursor(new String[]{"_id", name});
+ for (int i = 0; i < count; i++) {
+ cursor.addRow(new Object[]{i, name + "[" + i + "]"});
+ }
+ return cursor;
+ }
+}
diff --git a/src/com/android/contacts/widget/PinnedHeaderListView.java b/src/com/android/contacts/widget/PinnedHeaderListView.java
new file mode 100644
index 0000000..c51db9e
--- /dev/null
+++ b/src/com/android/contacts/widget/PinnedHeaderListView.java
@@ -0,0 +1,505 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView.OnItemSelectedListener;
+
+/**
+ * A ListView that maintains a header pinned at the top of the list. The
+ * pinned header can be pushed up and dissolved as needed.
+ */
+public class PinnedHeaderListView extends ListView
+ implements OnScrollListener, OnItemSelectedListener {
+
+ /**
+ * Adapter interface. The list adapter must implement this interface.
+ */
+ public interface PinnedHeaderAdapter {
+
+ /**
+ * Returns the overall number of pinned headers, visible or not.
+ */
+ int getPinnedHeaderCount();
+
+ /**
+ * Creates or updates the pinned header view.
+ */
+ View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent);
+
+ /**
+ * Configures the pinned headers to match the visible list items. The
+ * adapter should call {@link PinnedHeaderListView#setHeaderPinnedAtTop},
+ * {@link PinnedHeaderListView#setHeaderPinnedAtBottom},
+ * {@link PinnedHeaderListView#setFadingHeader} or
+ * {@link PinnedHeaderListView#setHeaderInvisible}, for each header that
+ * needs to change its position or visibility.
+ */
+ void configurePinnedHeaders(PinnedHeaderListView listView);
+
+ /**
+ * Returns the list position to scroll to if the pinned header is touched.
+ * Return -1 if the list does not need to be scrolled.
+ */
+ int getScrollPositionForHeader(int viewIndex);
+ }
+
+ private static final int MAX_ALPHA = 255;
+ private static final int TOP = 0;
+ private static final int BOTTOM = 1;
+ private static final int FADING = 2;
+
+ private static final int DEFAULT_ANIMATION_DURATION = 100;
+
+ private static final class PinnedHeader {
+ View view;
+ boolean visible;
+ int y;
+ int height;
+ int alpha;
+ int state;
+
+ boolean animating;
+ boolean targetVisible;
+ int sourceY;
+ int targetY;
+ long targetTime;
+ }
+
+ private PinnedHeaderAdapter mAdapter;
+ private int mSize;
+ private PinnedHeader[] mHeaders;
+ private int mPinnedHeaderBackgroundColor;
+ private RectF mBounds = new RectF();
+ private Paint mPaint = new Paint();
+ private OnScrollListener mOnScrollListener;
+ private OnItemSelectedListener mOnItemSelectedListener;
+ private int mScrollState;
+
+ private int mAnimationDuration = DEFAULT_ANIMATION_DURATION;
+ private boolean mAnimating;
+ private long mAnimationTargetTime;
+
+ public PinnedHeaderListView(Context context) {
+ this(context, null, com.android.internal.R.attr.listViewStyle);
+ }
+
+ public PinnedHeaderListView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.listViewStyle);
+ }
+
+ public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ super.setOnScrollListener(this);
+ super.setOnItemSelectedListener(this);
+ }
+
+ /**
+ * An approximation of the background color of the pinned header. This color
+ * is used when the pinned header is being pushed up. At that point the
+ * header "fades away". Rather than computing a faded bitmap based on the
+ * 9-patch normally used for the background, we will use a solid color,
+ * which will provide better performance and reduced complexity.
+ */
+ public void setPinnedHeaderBackgroundColor(int color) {
+ mPinnedHeaderBackgroundColor = color;
+ mPaint.setColor(mPinnedHeaderBackgroundColor);
+ }
+
+ public void setPinnedHeaderAnimationDuration(int duration) {
+ mAnimationDuration = duration;
+ }
+
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ mAdapter = (PinnedHeaderAdapter)adapter;
+ super.setAdapter(adapter);
+ }
+
+ @Override
+ public void setOnScrollListener(OnScrollListener onScrollListener) {
+ mOnScrollListener = onScrollListener;
+ super.setOnScrollListener(this);
+ }
+
+ @Override
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ super.setOnItemSelectedListener(this);
+ }
+
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+ if (mAdapter != null) {
+ int count = mAdapter.getPinnedHeaderCount();
+ if (count != mSize) {
+ mSize = count;
+ if (mHeaders == null) {
+ mHeaders = new PinnedHeader[mSize];
+ } else if (mHeaders.length < mSize) {
+ PinnedHeader[] headers = mHeaders;
+ mHeaders = new PinnedHeader[mSize];
+ System.arraycopy(headers, 0, mHeaders, 0, headers.length);
+ }
+ }
+
+ for (int i = 0; i < mSize; i++) {
+ if (mHeaders[i] == null) {
+ mHeaders[i] = new PinnedHeader();
+ }
+ mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this);
+ }
+
+ // Disable vertical fading when the pinned header is present
+ // TODO change ListView to allow separate measures for top and bottom fading edge;
+ // in this particular case we would like to disable the top, but not the bottom edge.
+ if (mSize > 0) {
+ setFadingEdgeLength(0);
+ }
+
+ mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration;
+ mAdapter.configurePinnedHeaders(this);
+ invalidateIfAnimating();
+
+ }
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount);
+ }
+ }
+
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ mScrollState = scrollState;
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollStateChanged(this, scrollState);
+ }
+ }
+
+ /**
+ * Ensures that the selected item is positioned below the top-pinned headers
+ * and above the bottom-pinned ones.
+ */
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ int height = getHeight();
+
+ int windowTop = 0;
+ int windowBottom = height;
+
+ int prevHeaderBottom = 0;
+ for (int i = 0; i < mSize; i++) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible) {
+ if (header.state == TOP) {
+ windowTop = header.y + header.height;
+ } else if (header.state == BOTTOM) {
+ windowBottom = header.y;
+ break;
+ }
+ }
+ }
+
+ View selectedView = getSelectedView();
+ if (selectedView.getTop() < windowTop) {
+ setSelectionFromTop(position, windowTop);
+ } else if (selectedView.getBottom() > windowBottom) {
+ setSelectionFromTop(position, windowBottom - selectedView.getHeight());
+ }
+
+ if (mOnItemSelectedListener != null) {
+ mOnItemSelectedListener.onItemSelected(parent, view, position, id);
+ }
+ }
+
+ public void onNothingSelected(AdapterView<?> parent) {
+ if (mOnItemSelectedListener != null) {
+ mOnItemSelectedListener.onNothingSelected(parent);
+ }
+ }
+
+ public int getPinnedHeaderHeight(int viewIndex) {
+ ensurePinnedHeaderLayout(viewIndex);
+ return mHeaders[viewIndex].view.getHeight();
+ }
+
+ /**
+ * Set header to be pinned at the top.
+ *
+ * @param viewIndex index of the header view
+ * @param y is position of the header in pixels.
+ * @param animate true if the transition to the new coordinate should be animated
+ */
+ public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) {
+ ensurePinnedHeaderLayout(viewIndex);
+ PinnedHeader header = mHeaders[viewIndex];
+ header.visible = true;
+ header.y = y;
+ header.state = TOP;
+
+ // TODO perhaps we should animate at the top as well
+ header.animating = false;
+ }
+
+ /**
+ * Set header to be pinned at the bottom.
+ *
+ * @param viewIndex index of the header view
+ * @param y is position of the header in pixels.
+ * @param animate true if the transition to the new coordinate should be animated
+ */
+ public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) {
+ ensurePinnedHeaderLayout(viewIndex);
+ PinnedHeader header = mHeaders[viewIndex];
+ header.state = BOTTOM;
+ if (header.animating) {
+ header.targetTime = mAnimationTargetTime;
+ header.targetY = y;
+ } else if (animate && (header.y != y || !header.visible)) {
+ if (header.visible) {
+ header.sourceY = y;
+ } else {
+ header.visible = true;
+ header.sourceY = y + header.height;
+ }
+ header.animating = true;
+ header.targetVisible = true;
+ header.targetTime = mAnimationTargetTime;
+ header.targetY = y;
+ } else {
+ header.visible = true;
+ header.y = y;
+ }
+ }
+
+ /**
+ * Set header to be pinned at the top of the first visible item.
+ *
+ * @param viewIndex index of the header view
+ * @param position is position of the header in pixels.
+ */
+ public void setFadingHeader(int viewIndex, int position, boolean fade) {
+ ensurePinnedHeaderLayout(viewIndex);
+
+ View child = getChildAt(position - getFirstVisiblePosition());
+
+ PinnedHeader header = mHeaders[viewIndex];
+ header.visible = true;
+ header.state = FADING;
+ header.alpha = MAX_ALPHA;
+ header.animating = false;
+
+ int top = getTotalTopPinnedHeaderHeight();
+ header.y = top;
+ if (fade) {
+ int bottom = child.getBottom() - top;
+ int headerHeight = header.height;
+ if (bottom < headerHeight) {
+ int portion = bottom - headerHeight;
+ header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight;
+ header.y = top + portion;
+ }
+ }
+ }
+
+ /**
+ * Makes header invisible.
+ *
+ * @param viewIndex index of the header view
+ * @param animate true if the transition to the new coordinate should be animated
+ */
+ public void setHeaderInvisible(int viewIndex, boolean animate) {
+ PinnedHeader header = mHeaders[viewIndex];
+ if (header.visible && (animate || header.animating) && header.state == BOTTOM) {
+ if (!header.animating) {
+ header.visible = true;
+ header.sourceY = header.y;
+ header.targetY = header.y + header.height;
+ }
+ header.animating = true;
+ header.targetTime = mAnimationTargetTime;
+ header.targetVisible = false;
+ } else {
+ header.visible = false;
+ }
+ }
+
+ private void ensurePinnedHeaderLayout(int viewIndex) {
+ View view = mHeaders[viewIndex].view;
+ if (view.isLayoutRequested()) {
+ int widthSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY);
+ int heightSpec;
+ int lpHeight = view.getLayoutParams().height;
+ if (lpHeight > 0) {
+ heightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
+ } else {
+ heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+ view.measure(widthSpec, heightSpec);
+ int height = view.getMeasuredHeight();
+ mHeaders[viewIndex].height = height;
+ view.layout(0, 0, view.getMeasuredWidth(), height);
+ }
+ }
+
+ /**
+ * Returns the sum of heights of headers pinned to the top.
+ */
+ public int getTotalTopPinnedHeaderHeight() {
+ for (int i = mSize; --i >= 0;) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible && header.state == TOP) {
+ return header.y + header.height;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Returns the list item position at the specified y coordinate.
+ */
+ public int getPositionAt(int y) {
+ do {
+ int position = pointToPosition(0, y);
+ if (position != -1) {
+ return position;
+ }
+ // If position == -1, we must have hit a separator. Let's examine
+ // a nearby pixel
+ y--;
+ } while (y > 0);
+ return 0;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (mScrollState == SCROLL_STATE_IDLE) {
+ final int y = (int)ev.getY();
+ for (int i = mSize; --i >= 0;) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible && header.y <= y && header.y + header.height > y) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ return smoothScrollToPartition(i);
+ } else {
+ return true;
+ }
+ }
+ }
+ }
+
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ private boolean smoothScrollToPartition(int partition) {
+ final int position = mAdapter.getScrollPositionForHeader(partition);
+ if (position == -1) {
+ return false;
+ }
+
+ smoothScrollToSelectionFromTop(position + getHeaderViewsCount(),
+ getTotalTopPinnedHeaderHeight());
+ return true;
+ }
+
+ public void smoothScrollToSelectionFromTop(final int position, int y) {
+ // This method is temporary. It will be replaced by new method on AbsListView
+ smoothScrollToPosition(position);
+
+ final int offset = y;
+ postDelayed(new Runnable() {
+
+ public void run() {
+ setSelectionFromTop(position, offset);
+ }
+ }, 500);
+ }
+
+ private void invalidateIfAnimating() {
+ mAnimating = false;
+ for (int i = 0; i < mSize; i++) {
+ if (mHeaders[i].animating) {
+ mAnimating = true;
+ invalidate();
+ return;
+ }
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ long currentTime = mAnimating ? System.currentTimeMillis() : 0;
+
+ // First draw top headers, then the bottom ones to handle the Z axis correctly
+ for (int i = mSize; --i >= 0;) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible && (header.state == TOP || header.state == FADING)) {
+ drawHeader(canvas, header, currentTime);
+ }
+ }
+
+ for (int i = 0; i < mSize; i++) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible && header.state == BOTTOM) {
+ drawHeader(canvas, header, currentTime);
+ }
+ }
+
+ invalidateIfAnimating();
+ }
+
+ private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) {
+ if (header.animating) {
+ int timeLeft = (int)(header.targetTime - currentTime);
+ if (timeLeft <= 0) {
+ header.y = header.targetY;
+ header.visible = header.targetVisible;
+ header.animating = false;
+ } else {
+ header.y = header.targetY + (header.sourceY - header.targetY) * timeLeft
+ / mAnimationDuration;
+ }
+ }
+ if (header.visible) {
+ View view = header.view;
+ if (header.state == FADING) {
+ int saveCount = canvas.save();
+ canvas.translate(0, header.y);
+ mBounds.set(0, 0, view.getWidth(), view.getHeight());
+ canvas.drawRect(mBounds, mPaint);
+ canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG);
+ view.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ } else {
+ canvas.save();
+ canvas.translate(0, header.y);
+ view.draw(canvas);
+ canvas.restore();
+ }
+ }
+ }
+}
diff --git a/src/com/android/contacts/widget/SearchEditText.java b/src/com/android/contacts/widget/SearchEditText.java
new file mode 100644
index 0000000..45001a5
--- /dev/null
+++ b/src/com/android/contacts/widget/SearchEditText.java
@@ -0,0 +1,130 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+/**
+ * A custom text editor that helps automatically dismiss the activity along with the soft
+ * keyboard.
+ */
+public class SearchEditText extends EditText implements OnEditorActionListener, TextWatcher {
+ private boolean mMagnifyingGlassShown = true;
+
+ private Drawable mMagnifyingGlass;
+ private OnFilterTextListener mListener;
+
+ public interface OnFilterTextListener {
+ void onFilterChange(String queryString);
+ void onCancelSearch();
+ }
+
+ public SearchEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ addTextChangedListener(this);
+ setOnEditorActionListener(this);
+ mMagnifyingGlass = getCompoundDrawables()[2];
+ }
+
+ public void setOnFilterTextListener(OnFilterTextListener listener) {
+ this.mListener = listener;
+ }
+
+ /**
+ * Conditionally shows a magnifying glass icon on the right side of the text field
+ * when the text it empty.
+ */
+ @Override
+ public boolean onPreDraw() {
+ boolean emptyText = TextUtils.isEmpty(getText());
+ if (mMagnifyingGlassShown != emptyText) {
+ mMagnifyingGlassShown = emptyText;
+ if (mMagnifyingGlassShown) {
+ setCompoundDrawables(null, null, mMagnifyingGlass, null);
+ } else {
+ setCompoundDrawables(null, null, null, null);
+ }
+ return false;
+ }
+ return super.onPreDraw();
+ }
+
+ /**
+ * Dismisses the search UI along with the keyboard if the filter text is empty.
+ */
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && TextUtils.isEmpty(getText()) && mListener != null) {
+ mListener.onCancelSearch();
+ return true;
+ }
+ return false;
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ /**
+ * Event handler for search UI.
+ */
+ public void afterTextChanged(Editable s) {
+ if (mListener != null) {
+ mListener.onFilterChange(trim(s));
+ }
+ }
+
+ private String trim(Editable s) {
+ return s.toString().trim();
+ }
+
+ /**
+ * Event handler for search UI.
+ */
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ hideSoftKeyboard();
+ if (TextUtils.isEmpty(trim(getText())) && mListener != null) {
+ mListener.onCancelSearch();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private void hideSoftKeyboard() {
+ // Hide soft keyboard, if visible
+ InputMethodManager inputMethodManager = (InputMethodManager)
+ getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+
+}
diff --git a/src/com/android/contacts/widget/SingleItemAdapter.java b/src/com/android/contacts/widget/SingleItemAdapter.java
new file mode 100644
index 0000000..3532bfc
--- /dev/null
+++ b/src/com/android/contacts/widget/SingleItemAdapter.java
@@ -0,0 +1,47 @@
+/*
+ * 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.widget;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * A general purpose adapter that contains exactly one item.
+ */
+public abstract class SingleItemAdapter extends BaseAdapter {
+
+ public int getCount() {
+ return 1;
+ }
+
+ public Object getItem(int position) {
+ return null;
+ }
+
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return getView(convertView, parent);
+ }
+
+ /**
+ * Creates the view.
+ */
+ protected abstract View getView(View convertView, ViewGroup parent);
+}
diff --git a/src/com/android/contacts/TextHighlightingAnimation.java b/src/com/android/contacts/widget/TextHighlightingAnimation.java
similarity index 96%
rename from src/com/android/contacts/TextHighlightingAnimation.java
rename to src/com/android/contacts/widget/TextHighlightingAnimation.java
index e35ae1e..21bbc63 100644
--- a/src/com/android/contacts/TextHighlightingAnimation.java
+++ b/src/com/android/contacts/widget/TextHighlightingAnimation.java
@@ -13,14 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.contacts;
+package com.android.contacts.widget;
import com.android.internal.R;
import android.database.CharArrayBuffer;
import android.graphics.Color;
import android.os.Handler;
-import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.view.animation.AccelerateInterpolator;
@@ -29,7 +28,7 @@
/**
* An animation that alternately dims and brightens the non-highlighted portion of text.
*/
-public abstract class TextHighlightingAnimation implements Runnable {
+public abstract class TextHighlightingAnimation implements Runnable, TextWithHighlightingFactory {
private static final int MAX_ALPHA = 255;
private static final int MIN_ALPHA = 50;
@@ -54,7 +53,7 @@
/**
* A Spanned that highlights a part of text by dimming another part of that text.
*/
- public class TextWithHighlighting implements Spanned {
+ public class TextWithHighlightingImpl implements TextWithHighlighting {
private final DimmingSpan[] mSpans;
private boolean mDimmingEnabled;
@@ -63,7 +62,7 @@
private int mDimmingSpanEnd;
private String mString;
- public TextWithHighlighting() {
+ public TextWithHighlightingImpl() {
mSpans = new DimmingSpan[] { mDimmingSpan };
}
@@ -216,12 +215,12 @@
/**
* Returns a Spanned that can be used by a text view to show text with highlighting.
*/
- public TextWithHighlighting createTextWithHighlighting() {
- return new TextWithHighlighting();
+ public TextWithHighlightingImpl createTextWithHighlighting() {
+ return new TextWithHighlightingImpl();
}
/**
- * Override and invalidate (redraw) TextViews showing {@link TextWithHighlighting}.
+ * Override and invalidate (redraw) TextViews showing {@link TextWithHighlightingImpl}.
*/
protected abstract void invalidate();
diff --git a/src/com/android/contacts/widget/TextWithHighlighting.java b/src/com/android/contacts/widget/TextWithHighlighting.java
new file mode 100644
index 0000000..3a32b02
--- /dev/null
+++ b/src/com/android/contacts/widget/TextWithHighlighting.java
@@ -0,0 +1,26 @@
+/*
+ * 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.widget;
+
+import android.database.CharArrayBuffer;
+import android.text.Spanned;
+
+/**
+ * A Spanned that highlights a part of text by dimming another part of that text.
+ */
+public interface TextWithHighlighting extends Spanned {
+ void setText(CharArrayBuffer baseText, CharArrayBuffer highlightedText);
+}
diff --git a/src/com/android/contacts/widget/TextWithHighlightingFactory.java b/src/com/android/contacts/widget/TextWithHighlightingFactory.java
new file mode 100644
index 0000000..ee5744d
--- /dev/null
+++ b/src/com/android/contacts/widget/TextWithHighlightingFactory.java
@@ -0,0 +1,23 @@
+/*
+ * 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.widget;
+
+/**
+ * A factory for text fields with animated highlighting.
+ */
+public interface TextWithHighlightingFactory {
+ TextWithHighlighting createTextWithHighlighting();
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 7af1a54..0c5ee70 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -4,9 +4,9 @@
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.
@@ -17,25 +17,57 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.contacts.tests">
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+
<application>
<uses-library android:name="android.test.runner" />
<meta-data android:name="com.android.contacts.iconset" android:resource="@xml/iconset" />
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+
+ <activity android:name=".allintents.AllIntentsActivity"
+ android:label="@string/contactsIntents"
+ >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".allintents.ResultActivity"
+ android:label="@string/result"
+ >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".widget.PinnedHeaderUseCaseActivity"
+ android:label="@string/pinnedHeaderList"
+ >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
</application>
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.android.contacts"
android:label="Contacts app tests">
</instrumentation>
-
+
<instrumentation android:name="com.android.contacts.ContactsLaunchPerformance"
android:targetPackage="com.android.contacts"
android:label="Contacts launch performance">
</instrumentation>
-
<instrumentation android:name="com.android.contacts.DialerLaunchPerformance"
android:targetPackage="com.android.contacts"
android:label="Dialer launch performance">
</instrumentation>
-</manifest>
+</manifest>
diff --git a/tests/assets/v21_simple.vcf b/tests/assets/v21_simple.vcf
new file mode 100644
index 0000000..86f4d33
--- /dev/null
+++ b/tests/assets/v21_simple.vcf
@@ -0,0 +1,3 @@
+BEGIN:VCARD
+N:test
+END:VCARD
\ No newline at end of file
diff --git a/tests/res/layout/intent_list_item.xml b/tests/res/layout/intent_list_item.xml
new file mode 100644
index 0000000..4749224
--- /dev/null
+++ b/tests/res/layout/intent_list_item.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+/>
diff --git a/tests/res/layout/result.xml b/tests/res/layout/result.xml
new file mode 100644
index 0000000..0ab32c6
--- /dev/null
+++ b/tests/res/layout/result.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="true"
+>
+
+ <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/table"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:shrinkColumns="1"
+ android:stretchColumns="*">
+ </TableLayout>
+</ScrollView>
+
diff --git a/tests/res/values/donottranslate_strings.xml b/tests/res/values/donottranslate_strings.xml
new file mode 100644
index 0000000..93095e3
--- /dev/null
+++ b/tests/res/values/donottranslate_strings.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <string name="contactsIntents">Contacts Intents</string>
+ <string name="result">Result returned by activity</string>
+
+ <string-array name="allIntents">
+ <!-- List modes -->
+ <item>LIST_DEFAULT</item>
+ <item>LIST_ALL_CONTACTS_ACTION</item>
+ <item>LIST_CONTACTS_WITH_PHONES_ACTION</item>
+ <item>LIST_STARRED_ACTION</item>
+ <item>LIST_STARRED_ACTION (filter)</item>
+ <item>LIST_FREQUENT_ACTION</item>
+ <item>LIST_FREQUENT_ACTION (filter)</item>
+ <item>LIST_STREQUENT_ACTION</item>
+ <item>LIST_STREQUENT_ACTION (filter)</item>
+ <item>ACTION_PICK: contact</item>
+ <item>ACTION_PICK: contact (legacy)</item>
+ <item>ACTION_PICK: phone</item>
+ <item>ACTION_PICK: phone (legacy)</item>
+ <item>ACTION_PICK: postal</item>
+ <item>ACTION_PICK: postal (legacy)</item>
+ <item>ACTION_CREATE_SHORTCUT: contact</item>
+ <item>ACTION_CREATE_SHORTCUT: contact (filter)</item>
+ <item>ACTION_CREATE_SHORTCUT: dial</item>
+ <item>ACTION_CREATE_SHORTCUT: dial (filter)</item>
+ <item>ACTION_CREATE_SHORTCUT: message</item>
+ <item>ACTION_CREATE_SHORTCUT: message (filter)</item>
+ <item>ACTION_GET_CONTENT: contact</item>
+ <item>ACTION_GET_CONTENT: contact (filter)</item>
+ <item>ACTION_GET_CONTENT: contact (legacy)</item>
+ <item>ACTION_GET_CONTENT: contact (filter, legacy)</item>
+ <item>ACTION_GET_CONTENT: phone</item>
+ <item>ACTION_GET_CONTENT: phone (filter)</item>
+ <item>ACTION_GET_CONTENT: phone (legacy)</item>
+ <item>ACTION_GET_CONTENT: postal</item>
+ <item>ACTION_GET_CONTENT: postal (filter)</item>
+ <item>ACTION_GET_CONTENT: postal (legacy)</item>
+ <item>ACTION_INSERT_OR_EDIT</item>
+ <item>ACTION_SEARCH (call button)</item>
+ <item>ACTION_SEARCH: contact</item>
+ <item>ACTION_SEARCH: email</item>
+ <item>ACTION_SEARCH: phone</item>
+ <item>SEARCH_SUGGESTION_CLICKED (call button)</item>
+ <item>SEARCH_SUGGESTION_CLICKED: contact</item>
+ <item>SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED</item>
+ <item>SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED</item>
+ <item>TODO: JOIN_CONTACT</item>
+ <item>ACTION_GET_MULTIPLE_PHONES</item>
+
+ <!-- Edit Contact -->
+ <item>EDIT (content uri with only id)</item>
+ <item>EDIT (lookup uri without id)</item>
+ <item>EDIT (lookup uri)</item>
+ <item>EDIT (called for raw contact)</item>
+ <item>EDIT (legacy style uri)</item>
+ <item>EDIT (create new contact)</item>
+ <item>EDIT (create new raw contact)</item>
+ <item>EDIT (create new legacy)</item>
+ </string-array>
+
+ <string name="pinnedHeaderList">Pinned Headers</string>
+
+ <string-array name="pinnedHeaderUseCases">
+ <item>One short section - no headers</item>
+ <item>Two short sections with headers</item>
+ <item>Five short sections with headers</item>
+ </string-array>
+</resources>
diff --git a/tests/src/com/android/contacts/ContactDetailTest.java b/tests/src/com/android/contacts/ContactDetailTest.java
new file mode 100644
index 0000000..0b850b7
--- /dev/null
+++ b/tests/src/com/android/contacts/ContactDetailTest.java
@@ -0,0 +1,41 @@
+package com.android.contacts;
+
+import com.android.contacts.activities.ContactDetailActivity;
+import com.android.contacts.tests.mocks.ContactsMockContext;
+import com.android.contacts.tests.mocks.MockContentProvider;
+
+import android.test.ActivityUnitTestCase;
+
+public class ContactDetailTest extends ActivityUnitTestCase<ContactDetailActivity> {
+ private ContactsMockContext mContext;
+ private MockContentProvider mContactsProvider;
+
+ public ContactDetailTest() {
+ super(ContactDetailActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mContext = new ContactsMockContext(getInstrumentation().getTargetContext());
+ mContactsProvider = mContext.getContactsProvider();
+ setActivityContext(mContext);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+// public void testFoo() {
+// // Use lookup-style Uris that also contain the Contact-ID
+// //long rawContactId1 = mCreator.createRawContact("JohnDoe", "John", "Doe");
+// //long contactId1 = mCreator.getContactIdByRawContactId(rawContactId1);
+// //Uri contactUri1 = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId1);
+// Intent intent = new Intent(Intent.ACTION_VIEW,
+// ContentUris.withAppendedId(Contacts.CONTENT_URI, 123));
+// startActivity(intent, null, null);
+// ContactDetailActivity activity = getActivity();
+// mContactsProvider.verify();
+// }
+}
diff --git a/tests/src/com/android/contacts/ContactListModeTest.java b/tests/src/com/android/contacts/ContactListModeTest.java
new file mode 100644
index 0000000..f09cc8d
--- /dev/null
+++ b/tests/src/com/android/contacts/ContactListModeTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+/**
+ * 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();
+
+ ListView listView = (ListView)activity.findViewById(android.R.id.list);
+ ListAdapter adapter = listView.getAdapter();
+ assertEquals(3, adapter.getCount());
+ }
+}
diff --git a/tests/src/com/android/contacts/ContactLoaderTest.java b/tests/src/com/android/contacts/ContactLoaderTest.java
new file mode 100644
index 0000000..8b58ee1
--- /dev/null
+++ b/tests/src/com/android/contacts/ContactLoaderTest.java
@@ -0,0 +1,574 @@
+/*
+ * 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.tests.mocks.ContactsMockContext;
+import com.android.contacts.tests.mocks.MockContentProvider;
+import com.android.contacts.views.ContactLoader;
+
+import android.content.ContentUris;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.RawContacts.Data;
+import android.provider.ContactsContract.RawContacts.Entity;
+import android.test.AndroidTestCase;
+import android.test.AssertionFailedError;
+
+import java.util.concurrent.ArrayBlockingQueue;
+
+/**
+ * Runs ContactLoader tests for the the contact-detail and editor view.
+ */
+public class ContactLoaderTest extends AndroidTestCase {
+ private ContactsMockContext mMockContext;
+ private MockContentProvider mContactsProvider;
+
+ static {
+ // Need to force class loading of AsyncTask on the main thread...
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... args) {return null;}
+ @Override
+ protected void onPostExecute(Void result) {}
+ };
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mMockContext = new ContactsMockContext(getContext());
+ mContactsProvider = mMockContext.getContactsProvider();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * 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");
+ }
+
+ /**
+ * Runs a Loader synchronously and returns the result of the load. The loader will
+ * be started, stopped, and destroyed by this method so it cannot be reused.
+ *
+ * @param loader The loader to run synchronously
+ * @return The result from the loader
+ */
+ private <T> T getLoaderResultSynchronously(final Loader<T> loader) {
+ // The test thread blocks on this queue until the loader puts it's result in
+ final ArrayBlockingQueue<T> queue = new ArrayBlockingQueue<T>(1);
+
+ // This callback runs on the "main" thread and unblocks the test thread
+ // when it puts the result into the blocking queue
+ final OnLoadCompleteListener<T> listener = new OnLoadCompleteListener<T>() {
+ public void onLoadComplete(Loader<T> completedLoader, T data) {
+ // Shut the loader down
+ completedLoader.unregisterListener(this);
+ completedLoader.stopLoading();
+ completedLoader.destroy();
+
+ // Store the result, unblocking the test thread
+ queue.add(data);
+ }
+ };
+
+ // This handler runs on the "main" thread of the process since AsyncTask
+ // is documented as needing to run on the main thread and many Loaders use
+ // AsyncTask
+ final Handler mainThreadHandler = new Handler(Looper.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ loader.registerListener(0, listener);
+ loader.startLoading();
+ }
+ };
+
+ // Ask the main thread to start the loading process
+ mainThreadHandler.sendEmptyMessage(0);
+
+ // Block on the queue waiting for the result of the load to be inserted
+ T result;
+ while (true) {
+ try {
+ result = queue.take();
+ break;
+ } catch (InterruptedException e) {
+ throw new RuntimeException("waiting thread interrupted", e);
+ }
+ }
+
+ return result;
+ }
+
+ private ContactLoader.Result assertLoadContact(Uri uri) {
+ final ContactLoader loader = new ContactLoader(mMockContext, uri);
+ return getLoaderResultSynchronously(loader);
+ }
+
+ public void testNullUri() {
+ ContactLoader.Result result = assertLoadContact(null);
+ assertEquals(ContactLoader.Result.ERROR, result);
+ }
+
+ public void testEmptyUri() {
+ ContactLoader.Result result = assertLoadContact(Uri.EMPTY);
+ assertEquals(ContactLoader.Result.ERROR, result);
+ }
+
+ public void testInvalidUri() {
+ ContactLoader.Result result = assertLoadContact(Uri.parse("content://wtf"));
+ assertEquals(ContactLoader.Result.ERROR, result);
+ }
+
+ public void testLoadContactWithContactIdUri() {
+ // Use content Uris that only contain the ID
+ final long contactId = 1;
+ final long rawContactId = 11;
+ final long dataId = 21;
+
+ final String encodedLookup = Uri.encode("aa%12%@!");
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+ contactId);
+ final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ mContactsProvider.expectTypeQuery(baseUri, Contacts.CONTENT_ITEM_TYPE);
+ queries.fetchLookupAndId(baseUri, contactId, encodedLookup);
+ queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+ queries.fetchSocial(dataUri, contactId);
+ queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+ ContactLoader.Result contact = assertLoadContact(baseUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(encodedLookup, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getEntities().size());
+ assertEquals(1, contact.getStatuses().size());
+ mContactsProvider.verify();
+ }
+
+ public void testLoadContactWithOldStyleUri() {
+ // Use content Uris that only contain the ID but use the format used in Donut
+ final long contactId = 1;
+ final long rawContactId = 11;
+ final long dataId = 21;
+
+ final String encodedLookup = Uri.encode("aa%12%@!");
+ final Uri legacyUri = ContentUris.withAppendedId(
+ Uri.parse("content://contacts"), rawContactId);
+ final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+ contactId);
+ final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ queries.fetchContactIdAndLookupFromRawContactUri(rawContactUri, contactId, encodedLookup);
+ queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+ queries.fetchSocial(dataUri, contactId);
+ queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+ ContactLoader.Result contact = assertLoadContact(legacyUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(encodedLookup, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getEntities().size());
+ assertEquals(1, contact.getStatuses().size());
+ mContactsProvider.verify();
+ }
+
+ public void testLoadContactWithRawContactIdUri() {
+ // Use content Uris that only contain the ID but use the format used in Donut
+ final long contactId = 1;
+ final long rawContactId = 11;
+ final long dataId = 21;
+
+ final String encodedLookup = Uri.encode("aa%12%@!");
+ final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+ contactId);
+ final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ mContactsProvider.expectTypeQuery(rawContactUri, RawContacts.CONTENT_ITEM_TYPE);
+ queries.fetchContactIdAndLookupFromRawContactUri(rawContactUri, contactId, encodedLookup);
+ queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+ queries.fetchSocial(dataUri, contactId);
+ queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+ ContactLoader.Result contact = assertLoadContact(rawContactUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(encodedLookup, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getEntities().size());
+ assertEquals(1, contact.getStatuses().size());
+ mContactsProvider.verify();
+ }
+
+ public void testLoadContactWithContactLookupUri() {
+ // Use lookup-style Uris that do not contain the Contact-ID
+
+ final long contactId = 1;
+ final long rawContactId = 11;
+ final long dataId = 21;
+
+ final String encodedLookup = Uri.encode("aa%12%@!");
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri lookupNoIdUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup);
+ final Uri lookupUri = ContentUris.withAppendedId(lookupNoIdUri, contactId);
+ final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ mContactsProvider.expectTypeQuery(lookupNoIdUri, Contacts.CONTENT_ITEM_TYPE);
+ queries.fetchLookupAndId(lookupNoIdUri, contactId, encodedLookup);
+ queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+ queries.fetchSocial(dataUri, contactId);
+ queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+ ContactLoader.Result contact = assertLoadContact(lookupNoIdUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(encodedLookup, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getEntities().size());
+ assertEquals(1, contact.getStatuses().size());
+ mContactsProvider.verify();
+ }
+
+ public void testLoadContactWithContactLookupAndIdUri() {
+ // Use lookup-style Uris that also contain the Contact-ID
+ final long contactId = 1;
+ final long rawContactId = 11;
+ final long dataId = 21;
+
+ final String encodedLookup = Uri.encode("aa%12%@!");
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+ contactId);
+ final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ mContactsProvider.expectTypeQuery(lookupUri, Contacts.CONTENT_ITEM_TYPE);
+ queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+ queries.fetchSocial(dataUri, contactId);
+ queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+ ContactLoader.Result contact = assertLoadContact(lookupUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(encodedLookup, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getEntities().size());
+ assertEquals(1, contact.getStatuses().size());
+ mContactsProvider.verify();
+ }
+
+ public void testLoadContactWithContactLookupWithIncorrectIdUri() {
+ // Use lookup-style Uris that contain incorrect Contact-ID
+ // (we want to ensure that still the correct contact is chosen)
+ // In this test, the incorrect Id references another Contact
+
+ final long contactId = 1;
+ final long wrongContactId = 2;
+ final long rawContactId = 11;
+ final long wrongRawContactId = 12;
+ final long dataId = 21;
+
+ final String encodedLookup = Uri.encode("aa%12%@!");
+ final String wrongEncodedLookup = Uri.encode("ab%12%@!");
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri wrongBaseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, wrongContactId);
+ final Uri lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+ contactId);
+ final Uri lookupWithWrongIdUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+ wrongContactId);
+ final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ mContactsProvider.expectTypeQuery(lookupWithWrongIdUri, Contacts.CONTENT_ITEM_TYPE);
+ queries.fetchHeaderData(wrongBaseUri, wrongRawContactId, wrongEncodedLookup);
+ queries.fetchLookupAndId(lookupWithWrongIdUri, contactId, encodedLookup);
+ queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+ queries.fetchSocial(dataUri, contactId);
+ queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+ ContactLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(encodedLookup, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getEntities().size());
+ assertEquals(1, contact.getStatuses().size());
+
+ mContactsProvider.verify();
+ }
+
+ public void testLoadContactWithContactLookupWithIncorrectIdUri2() {
+ // Use lookup-style Uris that contain incorrect Contact-ID
+ // (we want to ensure that still the correct contact is chosen)
+ // In this test, the incorrect Id references no contact
+
+ final long contactId = 1;
+ final long wrongContactId = 2;
+ final long rawContactId = 11;
+ final long wrongRawContactId = 12;
+ final long dataId = 21;
+
+ final String encodedLookup = Uri.encode("aa%12%@!");
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri wrongBaseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, wrongContactId);
+ final Uri lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+ contactId);
+ final Uri lookupWithWrongIdUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+ wrongContactId);
+ final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ mContactsProvider.expectTypeQuery(lookupWithWrongIdUri, Contacts.CONTENT_ITEM_TYPE);
+ queries.fetchHeaderDataNoResult(wrongBaseUri);
+ queries.fetchLookupAndId(lookupWithWrongIdUri, contactId, encodedLookup);
+ queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+ queries.fetchSocial(dataUri, contactId);
+ queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+ ContactLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
+
+ assertEquals(contactId, contact.getId());
+ assertEquals(rawContactId, contact.getNameRawContactId());
+ assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+ assertEquals(encodedLookup, contact.getLookupKey());
+ assertEquals(lookupUri, contact.getLookupUri());
+ assertEquals(1, contact.getEntities().size());
+ assertEquals(1, contact.getStatuses().size());
+
+ mContactsProvider.verify();
+ }
+
+ public void testLoadContactWithContactLookupWithIncorrectIdUri3() {
+ // Use lookup-style Uris that contain incorrect Contact-ID
+ // (we want to ensure that still the correct contact is chosen)
+ // In this test, the incorrect Id references no contact and the lookup
+ // key can also not be resolved
+
+ final long contactId = 1;
+ final long wrongContactId = 2;
+ final long rawContactId = 11;
+ final long wrongRawContactId = 12;
+ final long dataId = 21;
+
+ final String encodedLookup = Uri.encode("aa%12%@!");
+ final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri wrongBaseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, wrongContactId);
+ final Uri lookupUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+ contactId);
+ final Uri lookupWithWrongIdUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+ wrongContactId);
+ final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+ ContactQueries queries = new ContactQueries();
+ mContactsProvider.expectTypeQuery(lookupWithWrongIdUri, Contacts.CONTENT_ITEM_TYPE);
+ queries.fetchHeaderDataNoResult(wrongBaseUri);
+ queries.fetchLookupAndIdNoResult(lookupWithWrongIdUri);
+
+ ContactLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
+
+ assertEquals(ContactLoader.Result.NOT_FOUND, contact);
+
+ mContactsProvider.verify();
+ }
+
+ private class ContactQueries {
+ private void fetchRawContacts(final long contactId, final long dataId,
+ final long rawContactId) {
+ mContactsProvider.expectQuery(RawContactsEntity.CONTENT_URI)
+ .withDefaultProjection(new String[] {
+ RawContacts._ID, RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE,
+ RawContacts.DIRTY, RawContacts.VERSION, RawContacts.SOURCE_ID,
+ RawContacts.SYNC1, RawContacts.SYNC2, RawContacts.SYNC3, RawContacts.SYNC4,
+ RawContacts.DELETED, RawContacts.CONTACT_ID, RawContacts.STARRED,
+ RawContacts.IS_RESTRICTED, RawContacts.NAME_VERIFIED,
+
+ Entity.DATA_ID, Data.RES_PACKAGE, Data.MIMETYPE, Data.IS_PRIMARY,
+ Data.IS_SUPER_PRIMARY, Data.DATA_VERSION,
+ CommonDataKinds.GroupMembership.GROUP_SOURCE_ID,
+ Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
+ Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10,
+ Data.DATA11, Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
+ Data.SYNC1, Data.SYNC2, Data.SYNC3, Data.SYNC4
+ })
+ .withSelection(
+ RawContacts.CONTACT_ID + "=?",
+ new String[] { String.valueOf(contactId) } )
+ .returnRow(
+ rawContactId, "mockAccountName", "mockAccountType",
+ 0, 1, "aa%12%@!",
+ "", "", "", "",
+ 0, contactId, 0,
+ 0, 1,
+
+ dataId, "", StructuredName.CONTENT_ITEM_TYPE, 1,
+ 1, 1,
+ "mockGroupId",
+ "dat1", "dat2", "dat3", "dat4", "dat5",
+ "dat6", "dat7", "dat8", "dat9", "dat10",
+ "dat11", "dat12", "dat13", "dat14", null,
+ "syn1", "syn2", "syn3", "syn4");
+ }
+
+ private void fetchSocial(final Uri dataUri, final long expectedContactId) {
+ mContactsProvider.expectQuery(dataUri)
+ .withProjection(
+ Contacts._ID, StatusUpdates.STATUS, StatusUpdates.STATUS_RES_PACKAGE,
+ StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_LABEL,
+ StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.PRESENCE)
+ .withSelection(
+ StatusUpdates.PRESENCE +" IS NOT NULL OR " +
+ StatusUpdates.STATUS + " IS NOT NULL",
+ (String[]) null)
+ .returnRow(
+ expectedContactId, "This is a mock Status update", 0,
+ 1, 2,
+ 0, StatusUpdates.AVAILABLE);
+ }
+
+ private void fetchHeaderData(final Uri uri, final long expectedRawContactId,
+ final String expectedEncodedLookup) {
+ mContactsProvider.expectQuery(uri)
+ .withProjection(
+ Contacts.NAME_RAW_CONTACT_ID,
+ Contacts.DISPLAY_NAME_SOURCE,
+ Contacts.LOOKUP_KEY,
+ Contacts.DISPLAY_NAME,
+ Contacts.PHONETIC_NAME,
+ Contacts.PHOTO_ID,
+ Contacts.STARRED,
+ Contacts.CONTACT_PRESENCE,
+ Contacts.CONTACT_STATUS,
+ Contacts.CONTACT_STATUS_TIMESTAMP,
+ Contacts.CONTACT_STATUS_RES_PACKAGE,
+ Contacts.CONTACT_STATUS_LABEL)
+ .returnRow(
+ expectedRawContactId,
+ DisplayNameSources.STRUCTURED_NAME,
+ expectedEncodedLookup,
+ "contactDisplayName",
+ "contactPhoneticName",
+ null,
+ 0,
+ null,
+ null,
+ null,
+ null,
+ null);
+ }
+
+ private void fetchHeaderDataNoResult(final Uri uri) {
+ mContactsProvider.expectQuery(uri)
+ .withProjection(
+ Contacts.NAME_RAW_CONTACT_ID,
+ Contacts.DISPLAY_NAME_SOURCE,
+ Contacts.LOOKUP_KEY,
+ Contacts.DISPLAY_NAME,
+ Contacts.PHONETIC_NAME,
+ Contacts.PHOTO_ID,
+ Contacts.STARRED,
+ Contacts.CONTACT_PRESENCE,
+ Contacts.CONTACT_STATUS,
+ Contacts.CONTACT_STATUS_TIMESTAMP,
+ Contacts.CONTACT_STATUS_RES_PACKAGE,
+ Contacts.CONTACT_STATUS_LABEL);
+ }
+
+ private void fetchLookupAndId(final Uri sourceUri, final long expectedContactId,
+ final String expectedEncodedLookup) {
+ mContactsProvider.expectQuery(sourceUri)
+ .withProjection(Contacts.LOOKUP_KEY, Contacts._ID)
+ .returnRow(expectedEncodedLookup, expectedContactId);
+ }
+
+ private void fetchLookupAndIdNoResult(final Uri sourceUri) {
+ mContactsProvider.expectQuery(sourceUri)
+ .withProjection(Contacts.LOOKUP_KEY, Contacts._ID);
+ }
+
+ private void fetchContactIdAndLookupFromRawContactUri(final Uri rawContactUri,
+ final long expectedContactId, final String expectedEncodedLookup) {
+ // TODO: use a lighter query by joining rawcontacts with contacts in provider
+ // (See ContactContracts.java)
+ final Uri dataUri = Uri.withAppendedPath(rawContactUri, Data.CONTENT_DIRECTORY);
+ mContactsProvider.expectQuery(dataUri)
+ .withProjection(RawContacts.CONTACT_ID, Contacts.LOOKUP_KEY)
+ .returnRow(expectedContactId, expectedEncodedLookup);
+ }
+ }
+}
diff --git a/tests/src/com/android/contacts/tests/allintents/AllIntentsActivity.java b/tests/src/com/android/contacts/tests/allintents/AllIntentsActivity.java
new file mode 100644
index 0000000..b2c3072
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/allintents/AllIntentsActivity.java
@@ -0,0 +1,521 @@
+/*
+ * 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.allintents;
+
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.tests.R;
+
+import android.app.ListActivity;
+import android.app.SearchManager;
+import android.content.ComponentName;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.People;
+import android.provider.Contacts.Phones;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Intents.Insert;
+import android.provider.ContactsContract.Intents.UI;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.Toast;
+
+/**
+ * An activity that provides access to various modes of the contacts application.
+ * Useful for manual and scripted tests.
+ */
+@SuppressWarnings("deprecation")
+public class AllIntentsActivity extends ListActivity {
+
+ private static final String ANDROID_CONTACTS_PACKAGE = "com.android.contacts";
+
+ private static final String CONTACTS_LIST_ACTIVITY_CLASS_NAME =
+ "com.android.contacts.ContactsListActivity";
+ private static final String SEARCH_RESULTS_ACTIVITY_CLASS_NAME =
+ "com.android.contacts.SearchResultsActivity";
+ private static final String MULTIPLE_PHONE_PICKER_ACTIVITY_CLASS_NAME =
+ "com.android.contacts.MultiplePhonePickerActivity";
+
+ private static final int LIST_DEFAULT = 0;
+ private static final int LIST_ALL_CONTACTS_ACTION = 1;
+ private static final int LIST_CONTACTS_WITH_PHONES_ACTION = 2;
+ private static final int LIST_STARRED_ACTION = 3;
+ private static final int LIST_STARRED_ACTION_WITH_FILTER = 4;
+ private static final int LIST_FREQUENT_ACTION = 5;
+ private static final int LIST_FREQUENT_ACTION_WITH_FILTER = 6;
+ private static final int LIST_STREQUENT_ACTION = 7;
+ private static final int LIST_STREQUENT_ACTION_WITH_FILTER = 8;
+ private static final int ACTION_PICK_CONTACT = 9;
+ private static final int ACTION_PICK_CONTACT_LEGACY = 10;
+ private static final int ACTION_PICK_PHONE = 11;
+ private static final int ACTION_PICK_PHONE_LEGACY = 12;
+ private static final int ACTION_PICK_POSTAL = 13;
+ private static final int ACTION_PICK_POSTAL_LEGACY = 14;
+ private static final int ACTION_CREATE_SHORTCUT_CONTACT = 15;
+ private static final int ACTION_CREATE_SHORTCUT_CONTACT_FILTER = 16;
+ private static final int ACTION_CREATE_SHORTCUT_DIAL = 17;
+ private static final int ACTION_CREATE_SHORTCUT_DIAL_FILTER = 18;
+ private static final int ACTION_CREATE_SHORTCUT_MESSAGE = 19;
+ private static final int ACTION_CREATE_SHORTCUT_MESSAGE_FILTER = 20;
+ private static final int ACTION_GET_CONTENT_CONTACT = 21;
+ private static final int ACTION_GET_CONTENT_CONTACT_FILTER = 22;
+ private static final int ACTION_GET_CONTENT_CONTACT_LEGACY = 23;
+ private static final int ACTION_GET_CONTENT_CONTACT_FILTER_LEGACY = 24;
+ private static final int ACTION_GET_CONTENT_PHONE = 25;
+ private static final int ACTION_GET_CONTENT_PHONE_FILTER = 26;
+ private static final int ACTION_GET_CONTENT_PHONE_LEGACY = 27;
+ private static final int ACTION_GET_CONTENT_POSTAL = 28;
+ private static final int ACTION_GET_CONTENT_POSTAL_FILTER = 29;
+ private static final int ACTION_GET_CONTENT_POSTAL_LEGACY = 30;
+ private static final int ACTION_INSERT_OR_EDIT = 31;
+ private static final int ACTION_SEARCH_CALL = 32;
+ private static final int ACTION_SEARCH_CONTACT = 33;
+ private static final int ACTION_SEARCH_EMAIL = 34;
+ private static final int ACTION_SEARCH_PHONE = 35;
+ private static final int SEARCH_SUGGESTION_CLICKED_CALL_BUTTON = 36;
+ private static final int SEARCH_SUGGESTION_CLICKED_CONTACT = 37;
+ private static final int SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED = 38;
+ private static final int SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED = 39;
+ private static final int JOIN_CONTACT = 40;
+ private static final int ACTION_GET_MULTIPLE_PHONES = 41;
+
+ private static final int EDIT_CONTACT = 42;
+ private static final int EDIT_CONTACT_LOOKUP = 43;
+ private static final int EDIT_CONTACT_LOOKUP_ID = 44;
+ private static final int EDIT_RAW_CONTACT = 45;
+ private static final int EDIT_LEGACY = 46;
+ private static final int EDIT_NEW_CONTACT = 47;
+ private static final int EDIT_NEW_RAW_CONTACT = 48;
+ private static final int EDIT_NEW_LEGACY = 49;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setListAdapter(new ArrayAdapter<String>(this, R.layout.intent_list_item,
+ getResources().getStringArray(R.array.allIntents)));
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ super.onListItemClick(l, v, position, id);
+
+ switch (position) {
+ case LIST_DEFAULT: {
+ startContactsListActivity(
+ new Intent(Intent.ACTION_VIEW, Contacts.CONTENT_URI));
+ break;
+ }
+ case LIST_ALL_CONTACTS_ACTION: {
+ startContactsListActivity(
+ new Intent(UI.LIST_ALL_CONTACTS_ACTION, Contacts.CONTENT_URI));
+ break;
+ }
+ case LIST_CONTACTS_WITH_PHONES_ACTION: {
+ startContactsListActivity(
+ new Intent(UI.LIST_CONTACTS_WITH_PHONES_ACTION, Contacts.CONTENT_URI));
+ break;
+ }
+ case LIST_STARRED_ACTION: {
+ startContactsListActivity(
+ new Intent(UI.LIST_STARRED_ACTION, Contacts.CONTENT_URI));
+ break;
+ }
+ case LIST_STARRED_ACTION_WITH_FILTER: {
+ startContactsListActivity(
+ buildFilterIntent(UI.LIST_STARRED_ACTION, null, null));
+ break;
+ }
+ case LIST_FREQUENT_ACTION: {
+ startContactsListActivity(
+ new Intent(UI.LIST_FREQUENT_ACTION, Contacts.CONTENT_URI));
+ break;
+ }
+ case LIST_FREQUENT_ACTION_WITH_FILTER: {
+ startContactsListActivity(
+ buildFilterIntent(UI.LIST_FREQUENT_ACTION, null, null));
+ break;
+ }
+ case LIST_STREQUENT_ACTION: {
+ startContactsListActivity(
+ new Intent(UI.LIST_STREQUENT_ACTION, Contacts.CONTENT_URI));
+ break;
+ }
+ case LIST_STREQUENT_ACTION_WITH_FILTER: {
+ startContactsListActivity(
+ buildFilterIntent(UI.LIST_STREQUENT_ACTION, null, null));
+ break;
+ }
+ case ACTION_PICK_CONTACT: {
+ startContactsListActivityForResult(
+ new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI));
+ break;
+ }
+ case ACTION_PICK_CONTACT_LEGACY: {
+ startContactsListActivityForResult(
+ new Intent(Intent.ACTION_PICK, People.CONTENT_URI));
+ break;
+ }
+ case ACTION_PICK_PHONE: {
+ startContactsListActivityForResult(
+ new Intent(Intent.ACTION_PICK, Phone.CONTENT_URI));
+ break;
+ }
+ case ACTION_PICK_PHONE_LEGACY: {
+ startContactsListActivityForResult(
+ new Intent(Intent.ACTION_PICK, Phones.CONTENT_URI));
+ break;
+ }
+ case ACTION_PICK_POSTAL: {
+ startContactsListActivityForResult(
+ new Intent(Intent.ACTION_PICK, StructuredPostal.CONTENT_URI));
+ break;
+ }
+ case ACTION_PICK_POSTAL_LEGACY: {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.setType(ContactMethods.CONTENT_POSTAL_TYPE);
+ startContactsListActivityForResult(intent);
+ break;
+ }
+ case ACTION_CREATE_SHORTCUT_CONTACT: {
+ Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
+ startContactsListActivityForResult(intent);
+ break;
+ }
+ case ACTION_CREATE_SHORTCUT_CONTACT_FILTER: {
+ startContactsListActivityForResult(
+ buildFilterIntent(Intent.ACTION_CREATE_SHORTCUT,
+ CONTACTS_LIST_ACTIVITY_CLASS_NAME, null));
+ break;
+ }
+ case ACTION_CREATE_SHORTCUT_DIAL: {
+ Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
+ intent.setComponent(
+ new ComponentName(ANDROID_CONTACTS_PACKAGE, "alias.DialShortcut"));
+ startActivityForResult(intent, 0);
+ break;
+ }
+ case ACTION_CREATE_SHORTCUT_DIAL_FILTER: {
+ startContactsListActivityForResult(
+ buildFilterIntent(Intent.ACTION_CREATE_SHORTCUT,
+ "alias.DialShortcut", null));
+ break;
+ }
+ case ACTION_CREATE_SHORTCUT_MESSAGE: {
+ Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
+ intent.setComponent(
+ new ComponentName(ANDROID_CONTACTS_PACKAGE, "alias.MessageShortcut"));
+ startActivityForResult(intent, 0);
+ break;
+ }
+ case ACTION_CREATE_SHORTCUT_MESSAGE_FILTER: {
+ startContactsListActivityForResult(
+ buildFilterIntent(Intent.ACTION_CREATE_SHORTCUT,
+ "alias.MessageShortcut", null));
+ break;
+ }
+ case ACTION_GET_CONTENT_CONTACT: {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType(Contacts.CONTENT_ITEM_TYPE);
+ startContactsListActivityForResult(intent);
+ break;
+ }
+ case ACTION_GET_CONTENT_CONTACT_LEGACY: {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType(People.CONTENT_ITEM_TYPE);
+ startContactsListActivityForResult(intent);
+ break;
+ }
+ case ACTION_GET_CONTENT_CONTACT_FILTER: {
+ startContactsListActivityForResult(
+ buildFilterIntent(Intent.ACTION_GET_CONTENT,
+ CONTACTS_LIST_ACTIVITY_CLASS_NAME,
+ Contacts.CONTENT_ITEM_TYPE));
+ break;
+ }
+ case ACTION_GET_CONTENT_CONTACT_FILTER_LEGACY: {
+ startContactsListActivityForResult(
+ buildFilterIntent(Intent.ACTION_GET_CONTENT,
+ CONTACTS_LIST_ACTIVITY_CLASS_NAME,
+ People.CONTENT_ITEM_TYPE));
+ break;
+ }
+ case ACTION_GET_CONTENT_PHONE: {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType(Phone.CONTENT_ITEM_TYPE);
+ startContactsListActivityForResult(intent);
+ break;
+ }
+ case ACTION_GET_CONTENT_PHONE_FILTER: {
+ startContactsListActivityForResult(
+ buildFilterIntent(Intent.ACTION_GET_CONTENT,
+ CONTACTS_LIST_ACTIVITY_CLASS_NAME,
+ Phone.CONTENT_ITEM_TYPE));
+ break;
+ }
+ case ACTION_GET_CONTENT_PHONE_LEGACY: {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType(Phones.CONTENT_ITEM_TYPE);
+ startContactsListActivityForResult(intent);
+ break;
+ }
+ case ACTION_GET_CONTENT_POSTAL: {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType(StructuredPostal.CONTENT_ITEM_TYPE);
+ startContactsListActivityForResult(intent);
+ break;
+ }
+ case ACTION_GET_CONTENT_POSTAL_FILTER: {
+ startContactsListActivityForResult(
+ buildFilterIntent(Intent.ACTION_GET_CONTENT,
+ CONTACTS_LIST_ACTIVITY_CLASS_NAME,
+ StructuredPostal.CONTENT_ITEM_TYPE));
+ break;
+ }
+ case ACTION_GET_CONTENT_POSTAL_LEGACY: {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType(ContactMethods.CONTENT_POSTAL_ITEM_TYPE);
+ startContactsListActivityForResult(intent);
+ break;
+ }
+ case ACTION_INSERT_OR_EDIT: {
+ Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+ startContactsListActivity(intent);
+ break;
+ }
+ case ACTION_SEARCH_CALL: {
+ Intent intent = new Intent(Intent.ACTION_SEARCH);
+ intent.putExtra(SearchManager.ACTION_MSG, "call");
+ intent.putExtra(SearchManager.QUERY, "800-4664-411");
+ startSearchResultActivity(intent);
+ break;
+ }
+ case ACTION_SEARCH_CONTACT: {
+ Intent intent = new Intent(Intent.ACTION_SEARCH);
+ intent.putExtra(SearchManager.QUERY, "a");
+ startSearchResultActivity(intent);
+ break;
+ }
+ case ACTION_SEARCH_EMAIL: {
+ Intent intent = new Intent(Intent.ACTION_SEARCH);
+ intent.putExtra(Insert.EMAIL, "a");
+ startSearchResultActivity(intent);
+ break;
+ }
+ case ACTION_SEARCH_PHONE: {
+ Intent intent = new Intent(Intent.ACTION_SEARCH);
+ intent.putExtra(Insert.PHONE, "800");
+ startSearchResultActivity(intent);
+ break;
+ }
+ case SEARCH_SUGGESTION_CLICKED_CALL_BUTTON: {
+ long contactId = findArbitraryContactWithPhoneNumber();
+ if (contactId != -1) {
+ Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ Intent intent = new Intent(Intents.SEARCH_SUGGESTION_CLICKED);
+ intent.setData(contactUri);
+ intent.putExtra(SearchManager.ACTION_MSG, "call");
+ startContactsListActivity(intent);
+ }
+ break;
+ }
+ case SEARCH_SUGGESTION_CLICKED_CONTACT: {
+ long contactId = findArbitraryContactWithPhoneNumber();
+ if (contactId != -1) {
+ Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ Intent intent = new Intent(Intents.SEARCH_SUGGESTION_CLICKED);
+ intent.setData(contactUri);
+ startContactsListActivity(intent);
+ }
+ break;
+ }
+ case SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED: {
+ Intent intent = new Intent(Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
+ intent.setData(Uri.parse("tel:800-4664411"));
+ startContactsListActivity(intent);
+ break;
+ }
+ case SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED: {
+ Intent intent = new Intent(Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
+ intent.setData(Uri.parse("tel:800-4664411"));
+ startContactsListActivity(intent);
+ break;
+ }
+ case JOIN_CONTACT: {
+ // TODO
+ break;
+ }
+ case ACTION_GET_MULTIPLE_PHONES: {
+ Intent intent = new Intent(Intents.ACTION_GET_MULTIPLE_PHONES);
+ intent.setType(Phone.CONTENT_TYPE);
+ intent.putExtra(Intents.EXTRA_PHONE_URIS, new Uri[] {
+ Uri.parse("tel:555-1212"), Uri.parse("tel:555-2121")
+ });
+ startMultiplePhoneSelectionActivityForResult(intent);
+ break;
+ }
+ case EDIT_CONTACT: {
+ final long contactId = findArbitraryContactWithPhoneNumber();
+ final Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Intent intent = new Intent(Intent.ACTION_EDIT, uri);
+ startActivity(intent);
+ break;
+ }
+ case EDIT_CONTACT_LOOKUP: {
+ final long contactId = findArbitraryContactWithPhoneNumber();
+ final Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri lookupUri = Contacts.getLookupUri(getContentResolver(), uri);
+ final String lookupKey = lookupUri.getPathSegments().get(2);
+ final Uri lookupWithoutIdUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI,
+ lookupKey);
+ final Intent intent = new Intent(Intent.ACTION_EDIT, lookupWithoutIdUri);
+ startActivity(intent);
+ break;
+ }
+ case EDIT_CONTACT_LOOKUP_ID: {
+ final long contactId = findArbitraryContactWithPhoneNumber();
+ final Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Uri lookupUri = Contacts.getLookupUri(getContentResolver(), uri);
+ final Intent intent = new Intent(Intent.ACTION_EDIT, lookupUri);
+ startActivity(intent);
+ break;
+ }
+ case EDIT_RAW_CONTACT: {
+ final long contactId = findArbitraryContactWithPhoneNumber();
+ final long rawContactId = findArbitraryRawContactOfContact(contactId);
+ final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ final Intent intent = new Intent(Intent.ACTION_EDIT, uri);
+ startActivity(intent);
+ break;
+ }
+ case EDIT_LEGACY: {
+ final Uri legacyContentUri = Uri.parse("content://contacts/people");
+ final long contactId = findArbitraryContactWithPhoneNumber();
+ final long rawContactId = findArbitraryRawContactOfContact(contactId);
+ final Uri uri = ContentUris.withAppendedId(legacyContentUri, rawContactId);
+ final Intent intent = new Intent(Intent.ACTION_EDIT, uri);
+ startActivity(intent);
+ break;
+ }
+ case EDIT_NEW_CONTACT: {
+ startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI));
+ break;
+ }
+ case EDIT_NEW_RAW_CONTACT: {
+ startActivity(new Intent(Intent.ACTION_INSERT, RawContacts.CONTENT_URI));
+ break;
+ }
+ case EDIT_NEW_LEGACY: {
+ final Uri legacyContentUri = Uri.parse("content://contacts/people");
+ startActivity(new Intent(Intent.ACTION_INSERT, legacyContentUri));
+ break;
+ }
+ default: {
+ Toast.makeText(this, "Sorry, we forgot to write this...", Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ private Intent buildFilterIntent(String action, String component, String type) {
+ Intent intent = new Intent(UI.FILTER_CONTACTS_ACTION);
+ intent.putExtra(UI.FILTER_TEXT_EXTRA_KEY, "A");
+ intent.putExtra(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY, action);
+ if (component != null) {
+ intent.putExtra(ContactsSearchManager.ORIGINAL_COMPONENT_EXTRA_KEY, component);
+ }
+ if (type != null) {
+ intent.putExtra(ContactsSearchManager.ORIGINAL_TYPE_EXTRA_KEY, type);
+ }
+ return intent;
+ }
+
+ private void startContactsListActivity(Intent intent) {
+ intent.setComponent(
+ new ComponentName(ANDROID_CONTACTS_PACKAGE, CONTACTS_LIST_ACTIVITY_CLASS_NAME));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+
+ private void startContactsListActivityForResult(Intent intent) {
+ intent.setComponent(
+ new ComponentName(ANDROID_CONTACTS_PACKAGE, CONTACTS_LIST_ACTIVITY_CLASS_NAME));
+ startActivityForResult(intent, 12);
+ }
+
+ private void startSearchResultActivity(Intent intent) {
+ intent.setComponent(
+ new ComponentName(ANDROID_CONTACTS_PACKAGE, SEARCH_RESULTS_ACTIVITY_CLASS_NAME));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+
+ private void startMultiplePhoneSelectionActivityForResult(Intent intent) {
+ intent.setComponent(
+ new ComponentName(ANDROID_CONTACTS_PACKAGE,
+ MULTIPLE_PHONE_PICKER_ACTIVITY_CLASS_NAME));
+ startActivityForResult(intent, 13);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Intent intent = new Intent(this, ResultActivity.class);
+ intent.putExtra("resultCode", resultCode);
+ intent.putExtra("data", data);
+ startActivity(intent);
+ }
+
+ private long findArbitraryContactWithPhoneNumber() {
+ final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
+ new String[] { Contacts._ID },
+ Contacts.HAS_PHONE_NUMBER + "!=0 AND " + Contacts.STARRED + "!=0" ,
+ null, "RANDOM() LIMIT 1");
+ try {
+ if (cursor.moveToFirst()) {
+ return cursor.getLong(0);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return -1;
+ }
+
+ private long findArbitraryRawContactOfContact(long contactId) {
+ final Cursor cursor = getContentResolver().query(RawContacts.CONTENT_URI,
+ new String[] { RawContacts._ID },
+ RawContacts.CONTACT_ID + "=?",
+ new String[] { String.valueOf(contactId) },
+ RawContacts._ID + " LIMIT 1");
+ try {
+ if (cursor.moveToFirst()) {
+ return cursor.getLong(0);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return -1;
+ }
+}
diff --git a/tests/src/com/android/contacts/tests/allintents/ResultActivity.java b/tests/src/com/android/contacts/tests/allintents/ResultActivity.java
new file mode 100644
index 0000000..562f2ba
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/allintents/ResultActivity.java
@@ -0,0 +1,196 @@
+/*
+ * 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.allintents;
+
+import com.android.contacts.tests.R;
+
+import android.app.Activity;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+import android.widget.ImageView.ScaleType;
+
+import java.util.Arrays;
+
+/**
+ * An activity that shows the result of a contacts activity invocation.
+ */
+public class ResultActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.result);
+
+ Intent intent = getIntent();
+ addRowsForIntent((Intent)intent.getExtras().get("data"));
+ }
+
+ private void addRowsForIntent(Intent intent) {
+ if (intent == null) {
+ addRow("", "No data intent returned");
+ } else {
+ addRow("INTENT", intent.toString());
+ addSeparator(3);
+
+ Bundle extras = intent.getExtras();
+ if (extras != null && !extras.isEmpty()) {
+ for (String key : extras.keySet()) {
+ Object value = extras.get(key);
+ addRow("EXTRA", key);
+ addRowForValue("", value);
+ }
+
+ addSeparator(3);
+ }
+
+ String dataUri = intent.getDataString();
+ if (dataUri != null) {
+ addRowsForQuery(Uri.parse(dataUri));
+ }
+ }
+ }
+
+ private void addRowForValue(String label, Object value) {
+ if (value == null) {
+ addRow(label, "null");
+ } else if (value instanceof Bitmap) {
+ addRowWithBitmap(label, (Bitmap)value);
+ } else if (value instanceof Intent) {
+ addRow(label, "INTENT");
+ addRowsForIntent((Intent)value);
+ } else if (value instanceof Uri) {
+ addRow(label, "DATA");
+ addRowsForQuery((Uri)value);
+ } else if (value.getClass().isArray()) {
+ addRow(label, "ARRAY");
+ Parcelable[] array = (Parcelable[])value;
+ for (int i = 0; i < array.length; i++) {
+ addRowForValue("[" + i + "]", String.valueOf(array[i]));
+ }
+ } else {
+ addRow(label, String.valueOf(value));
+ }
+ }
+
+ private void addRowsForQuery(Uri dataUri) {
+ Cursor cursor = getContentResolver().query(dataUri, null, null, null, null);
+ if (cursor == null) {
+ addRow("", "No data for this URI");
+ } else {
+ try {
+ while (cursor.moveToNext()) {
+ addRow("", "DATA");
+ String[] columnNames = cursor.getColumnNames();
+ String[] names = new String[columnNames.length];
+ System.arraycopy(columnNames, 0, names, 0, columnNames.length);
+ Arrays.sort(names);
+ for (int i = 0; i < names.length; i++) {
+ int index = cursor.getColumnIndex(names[i]);
+ String value = cursor.getString(index);
+ addRow(names[i], value);
+
+ if (names[i].equals(Contacts.PHOTO_ID) && !TextUtils.isEmpty(value)) {
+ addRowWithPhoto(Long.parseLong(value));
+ }
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ private void addRow(String column0, String column1) {
+ TextView label = new TextView(this);
+ label.setPadding(4, 4, 4, 4);
+ label.setText(column0);
+ TextView value = new TextView(this);
+ value.setPadding(4, 4, 4, 4);
+ value.setText(column1);
+ addRow(label, value);
+ }
+
+ private void addRowWithPhoto(long photoId) {
+ byte[] data = null;
+ Cursor cursor = getContentResolver().query(
+ ContentUris.withAppendedId(Data.CONTENT_URI, photoId),
+ new String[]{Photo.PHOTO}, null, null, null);
+ try {
+ if (cursor.moveToNext()) {
+ data = cursor.getBlob(0);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ if (data == null) {
+ return;
+ }
+
+ addRowWithBitmap("Photo", BitmapFactory.decodeByteArray(data, 0, data.length));
+ }
+
+ private void addRowWithBitmap(String label, Bitmap bitmap) {
+ TextView labelView = new TextView(this);
+ labelView.setPadding(4, 4, 4, 4);
+ labelView.setText(label);
+
+ ImageView imageView = new ImageView(this);
+ imageView.setImageBitmap(bitmap);
+ imageView.setPadding(4, 4, 4, 4);
+ imageView.setScaleType(ScaleType.FIT_START);
+ addRow(labelView, imageView);
+ }
+
+ private void addRow(View column0, View column1) {
+ TableLayout table = (TableLayout)findViewById(R.id.table);
+ TableRow row = new TableRow(this);
+ row.addView(column0);
+ row.addView(column1);
+ table.addView(row);
+
+ addSeparator(1);
+ }
+
+ private void addSeparator(int height) {
+ TableLayout table = (TableLayout)findViewById(R.id.table);
+ View separator = new View(this);
+ TableLayout.LayoutParams params = new TableLayout.LayoutParams();
+ params.height = height;
+ separator.setLayoutParams(params);
+ separator.setBackgroundColor(Color.rgb(33, 66, 33));
+ table.addView(separator);
+ }
+}
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..4697b83
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return this;
+ }
+}
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..83db553
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/mocks/MockContentProvider.java
@@ -0,0 +1,259 @@
+/*
+ * 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 {
+ private static final String TAG = "MockContentProvider";
+
+ 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 static 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;
+ }
+
+ if (array1.length != array2.length) 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;
+ }
+ }
+
+ public static class TypeQuery {
+ private final Uri mUri;
+ private final String mType;
+
+ public TypeQuery(Uri uri, String type) {
+ mUri = uri;
+ mType = type;
+ }
+
+ public Uri getUri() {
+ return mUri;
+ }
+
+ public String getType() {
+ return mType;
+ }
+
+ @Override
+ public String toString() {
+ return mUri + " --> " + mType;
+ }
+
+ public boolean equals(Uri uri) {
+ return getUri().equals(uri);
+ }
+ }
+
+ private LinkedList<Query> mExpectedQueries = new LinkedList<Query>();
+ private LinkedList<TypeQuery> mExpectedTypeQueries = new LinkedList<TypeQuery>();
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ public Query expectQuery(Uri contentUri) {
+ Query query = new Query(contentUri);
+ mExpectedQueries.offer(query);
+ return query;
+ }
+
+ public void expectTypeQuery(Uri uri, String type) {
+ TypeQuery result = new TypeQuery(uri, type);
+ mExpectedTypeQueries.offer(result);
+ }
+
+ @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) {
+ if (mExpectedTypeQueries.isEmpty()) {
+ Assert.fail("Unexpected getType query: " + uri);
+ }
+
+ TypeQuery query = mExpectedTypeQueries.remove();
+ if (!query.equals(uri)) {
+ Assert.fail("Incorrect query.\n Expected: " + query + "\n Actual: " + uri);
+ }
+
+ return query.getType();
+ }
+
+ @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 (selectionArgs != null) {
+ sb.append(Arrays.toString(selectionArgs));
+ } else {
+ sb.append("[]");
+ }
+ }
+ if (sortOrder != null) {
+ sb.append(" sort: '").append(sortOrder).append("'");
+ }
+ return sb.toString();
+ }
+
+ public void verify() {
+ Assert.assertTrue("Not all expected queries have been called: " +
+ mExpectedQueries, mExpectedQueries.isEmpty());
+ Assert.assertTrue("Not all expected getType-queries have been called: " +
+ mExpectedQueries, mExpectedTypeQueries.isEmpty());
+ }
+}
diff --git a/tests/src/com/android/contacts/tests/widget/PinnedHeaderUseCaseActivity.java b/tests/src/com/android/contacts/tests/widget/PinnedHeaderUseCaseActivity.java
new file mode 100644
index 0000000..b01963f
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/widget/PinnedHeaderUseCaseActivity.java
@@ -0,0 +1,89 @@
+/*
+ * 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.widget;
+
+import com.android.contacts.tests.R;
+import com.android.contacts.widget.PinnedHeaderListView;
+
+import android.app.ListActivity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+/**
+ * An activity that demonstrates various use cases for the {@link PinnedHeaderListView}.
+ */
+public class PinnedHeaderUseCaseActivity extends ListActivity {
+
+ private static final int SINGLE_SHORT_SECTION_NO_HEADERS = 0;
+ private static final int TWO_SHORT_SECTIONS_WITH_HEADERS = 1;
+ private static final int FIVE_SHORT_SECTIONS_WITH_HEADERS = 2;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setListAdapter(new ArrayAdapter<String>(this, R.layout.intent_list_item,
+ getResources().getStringArray(R.array.pinnedHeaderUseCases)));
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ switch (position) {
+ case SINGLE_SHORT_SECTION_NO_HEADERS:
+ startActivity(
+ new int[]{5},
+ new String[]{"Line"},
+ new boolean[]{false},
+ new boolean[]{false},
+ new int[]{0});
+ break;
+ case TWO_SHORT_SECTIONS_WITH_HEADERS:
+ startActivity(
+ new int[]{2, 30},
+ new String[]{"First", "Second"},
+ new boolean[]{true, true},
+ new boolean[]{false, false},
+ new int[]{0, 2000});
+ break;
+ case FIVE_SHORT_SECTIONS_WITH_HEADERS:
+ startActivity(
+ new int[]{1, 5, 5, 5, 5},
+ new String[]{"First", "Second", "Third", "Fourth", "Fifth"},
+ new boolean[]{true, true, true, true, true},
+ new boolean[]{false, false, false, false, false},
+ new int[]{0, 2000, 3000, 4000, 5000});
+ break;
+ }
+ }
+
+ private void startActivity(int[] counts, String[] names, boolean[] headers,
+ boolean[] showIfEmpty, int[] delays) {
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName("com.android.contacts",
+ "com.android.contacts.widget.PinnedHeaderListDemoActivity"));
+ intent.putExtra("counts", counts);
+ intent.putExtra("names", names);
+ intent.putExtra("headers", headers);
+ intent.putExtra("showIfEmpty", showIfEmpty);
+ intent.putExtra("delays", delays);
+
+ startActivity(intent);
+ }
+}
diff --git a/tests/src/com/android/contacts/vcard/ImportProcessorTest.java b/tests/src/com/android/contacts/vcard/ImportProcessorTest.java
new file mode 100644
index 0000000..543dce9
--- /dev/null
+++ b/tests/src/com/android/contacts/vcard/ImportProcessorTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.vcard;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.test.AndroidTestCase;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.vcard.VCardInterpreter;
+import com.android.vcard.VCardSourceDetector;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.util.List;
+
+public class ImportProcessorTest extends AndroidTestCase {
+ private static final String LOG_TAG = "ImportProcessorTest";
+ private ImportProcessor mImportProcessor;
+
+ private String mCopiedFileName;
+
+ // XXX: better way to copy stream?
+ private Uri copyToLocal(final String fileName) throws IOException {
+ final Context context = getContext();
+ // We need to use Context of this unit test runner (not of test to be tested),
+ // as only the former knows assets to be copied.
+ final Context testContext = getTestContext();
+ final ContentResolver resolver = testContext.getContentResolver();
+ mCopiedFileName = fileName;
+ ReadableByteChannel inputChannel = null;
+ WritableByteChannel outputChannel = null;
+ Uri destUri;
+ try {
+ inputChannel = Channels.newChannel(testContext.getAssets().open(fileName));
+ destUri = Uri.parse(context.getFileStreamPath(fileName).toURI().toString());
+ outputChannel =
+ getContext().openFileOutput(fileName,
+ Context.MODE_WORLD_WRITEABLE).getChannel();
+ final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
+ while (inputChannel.read(buffer) != -1) {
+ buffer.flip();
+ outputChannel.write(buffer);
+ buffer.compact();
+ }
+ buffer.flip();
+ while (buffer.hasRemaining()) {
+ outputChannel.write(buffer);
+ }
+ } finally {
+ if (inputChannel != null) {
+ try {
+ inputChannel.close();
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "Failed to close inputChannel.");
+ }
+ }
+ if (outputChannel != null) {
+ try {
+ outputChannel.close();
+ } catch(IOException e) {
+ Log.w(LOG_TAG, "Failed to close outputChannel");
+ }
+ }
+ }
+ return destUri;
+ }
+
+ @Override
+ public void setUp() {
+ mImportProcessor = new ImportProcessor(getContext());
+ mImportProcessor.ensureInit();
+ mCopiedFileName = null;
+ }
+
+ @Override
+ public void tearDown() {
+ if (!TextUtils.isEmpty(mCopiedFileName)) {
+ getContext().deleteFile(mCopiedFileName);
+ mCopiedFileName = null;
+ }
+ }
+
+ /**
+ * Confirm {@link ImportProcessor#readOneVCard(android.net.Uri, int, String,
+ * com.android.vcard.VCardInterpreter, int[])} successfully handles correct input.
+ */
+ public void testProcessSimple() throws IOException {
+ final Uri uri = copyToLocal("v21_simple.vcf");
+ final int vcardType = VCardSourceDetector.PARSE_TYPE_UNKNOWN;
+ final String charset = null;
+ final VCardInterpreter interpreter = new EmptyVCardInterpreter();
+ final int[] versions = new int[] {
+ ImportVCardActivity.VCARD_VERSION_V21
+ };
+
+ assertTrue(mImportProcessor.readOneVCard(
+ uri, vcardType, charset, interpreter, versions));
+ }
+}
+
+/* package */ class EmptyVCardInterpreter implements VCardInterpreter {
+ public void end() {
+ }
+
+ public void endEntry() {
+ }
+
+ public void endProperty() {
+ }
+
+ public void propertyGroup(String group) {
+ }
+
+ public void propertyName(String name) {
+ }
+
+ public void propertyParamType(String type) {
+ }
+
+ public void propertyParamValue(String value) {
+ }
+
+ public void propertyValues(List<String> values) {
+ }
+
+ public void start() {
+ }
+
+ public void startEntry() {
+ }
+
+ public void startProperty() {
+ }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/contacts/widget/CompositeCursorAdapterTest.java b/tests/src/com/android/contacts/widget/CompositeCursorAdapterTest.java
new file mode 100644
index 0000000..813d2be
--- /dev/null
+++ b/tests/src/com/android/contacts/widget/CompositeCursorAdapterTest.java
@@ -0,0 +1,251 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Tests for {@link CompositeCursorAdapter}.
+ */
+@SmallTest
+public class CompositeCursorAdapterTest extends AndroidTestCase {
+
+ public class TestCompositeCursorAdapter extends CompositeCursorAdapter {
+
+ public TestCompositeCursorAdapter() {
+ super(CompositeCursorAdapterTest.this.getContext());
+ }
+
+ private StringBuilder mRequests = new StringBuilder();
+
+ @Override
+ protected View newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent) {
+ return new View(context);
+ }
+
+ @Override
+ protected void bindHeaderView(View view, int partition, Cursor cursor) {
+ mRequests.append(partition + (cursor == null ? "" : cursor.getColumnNames()[0])
+ + "[H] ");
+ }
+
+ @Override
+ protected View newView(Context context, int sectionIndex, Cursor cursor, int position,
+ ViewGroup parent) {
+ return new View(context);
+ }
+
+ @Override
+ protected void bindView(View v, int partition, Cursor cursor, int position) {
+ if (!cursor.moveToPosition(position)) {
+ fail("Invalid position:" + partition + " " + cursor.getColumnNames()[0] + " "
+ + position);
+ }
+
+ mRequests.append(partition + cursor.getColumnNames()[0] + "["
+ + cursor.getInt(0) + "] ");
+ }
+
+ @Override
+ public String toString() {
+ return mRequests.toString().trim();
+ }
+ }
+
+ public void testGetCountNoEmptySections() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(false, false);
+ adapter.addPartition(false, false);
+
+ adapter.changeCursor(0, makeCursor("a", 2));
+ adapter.changeCursor(1, makeCursor("b", 3));
+
+ assertEquals(5, adapter.getCount());
+ }
+
+ public void testGetViewNoEmptySections() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(false, false);
+ adapter.addPartition(false, false);
+
+ adapter.changeCursor(0, makeCursor("a", 1));
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ for (int i = 0; i < adapter.getCount(); i++) {
+ adapter.getView(i, null, null);
+ }
+
+ assertEquals("0a[0] 1b[0] 1b[1]", adapter.toString());
+ }
+
+ public void testGetCountWithHeadersAndNoEmptySections() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(false, true);
+ adapter.addPartition(false, true);
+
+ adapter.changeCursor(0, makeCursor("a", 2));
+ adapter.changeCursor(1, makeCursor("b", 3));
+
+ assertEquals(7, adapter.getCount());
+ }
+
+ public void testGetViewWithHeadersNoEmptySections() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(false, true);
+ adapter.addPartition(false, true);
+
+ adapter.changeCursor(0, makeCursor("a", 1));
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ for (int i = 0; i < adapter.getCount(); i++) {
+ adapter.getView(i, null, null);
+ }
+
+ assertEquals("0a[H] 0a[0] 1b[H] 1b[0] 1b[1]", adapter.toString());
+ }
+
+ public void testGetCountWithHiddenEmptySection() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(false, true);
+ adapter.addPartition(false, true);
+
+ adapter.changeCursor(1, makeCursor("a", 2));
+
+ assertEquals(3, adapter.getCount());
+ }
+
+ public void testGetPartitionForPosition() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, false);
+ adapter.addPartition(true, true);
+
+ adapter.changeCursor(0, makeCursor("a", 1));
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ assertEquals(0, adapter.getPartitionForPosition(0));
+ assertEquals(1, adapter.getPartitionForPosition(1));
+ assertEquals(1, adapter.getPartitionForPosition(2));
+ assertEquals(1, adapter.getPartitionForPosition(3));
+ }
+
+ public void testGetOffsetForPosition() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, false);
+ adapter.addPartition(true, true);
+
+ adapter.changeCursor(0, makeCursor("a", 1));
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ assertEquals(0, adapter.getOffsetInPartition(0));
+ assertEquals(-1, adapter.getOffsetInPartition(1));
+ assertEquals(0, adapter.getOffsetInPartition(2));
+ assertEquals(1, adapter.getOffsetInPartition(3));
+ }
+
+ public void testGetPositionForPartition() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, true);
+ adapter.addPartition(true, true);
+
+ adapter.changeCursor(0, makeCursor("a", 1));
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ assertEquals(0, adapter.getPositionForPartition(0));
+ assertEquals(2, adapter.getPositionForPartition(1));
+ }
+
+ public void testGetViewWithHiddenEmptySections() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(false, false);
+ adapter.addPartition(false, false);
+
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ for (int i = 0; i < adapter.getCount(); i++) {
+ adapter.getView(i, null, null);
+ }
+
+ assertEquals("1b[0] 1b[1]", adapter.toString());
+ }
+
+ public void testGetCountWithShownEmptySection() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, true);
+ adapter.addPartition(true, true);
+
+ adapter.changeCursor(1, makeCursor("a", 2));
+
+ assertEquals(4, adapter.getCount());
+ }
+
+ public void testGetViewWithShownEmptySections() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, true);
+ adapter.addPartition(true, true);
+
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ for (int i = 0; i < adapter.getCount(); i++) {
+ adapter.getView(i, null, null);
+ }
+
+ assertEquals("0[H] 1b[H] 1b[0] 1b[1]", adapter.toString());
+ }
+
+ public void testAreAllItemsEnabledFalse() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, false);
+ adapter.addPartition(true, true);
+
+ assertFalse(adapter.areAllItemsEnabled());
+ }
+
+ public void testAreAllItemsEnabledTrue() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, false);
+ adapter.addPartition(true, false);
+
+ assertTrue(adapter.areAllItemsEnabled());
+ }
+
+ public void testIsEnabled() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, false);
+ adapter.addPartition(true, true);
+
+ adapter.changeCursor(0, makeCursor("a", 1));
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ assertTrue(adapter.isEnabled(0));
+ assertFalse(adapter.isEnabled(1));
+ assertTrue(adapter.isEnabled(2));
+ assertTrue(adapter.isEnabled(3));
+ }
+
+ private Cursor makeCursor(String name, int count) {
+ MatrixCursor cursor = new MatrixCursor(new String[]{name});
+ for (int i = 0; i < count; i++) {
+ cursor.addRow(new Object[]{i});
+ }
+ return cursor;
+ }
+}
diff --git a/tests/src/com/android/contacts/widget/CompositeListAdapterTest.java b/tests/src/com/android/contacts/widget/CompositeListAdapterTest.java
new file mode 100644
index 0000000..87d268b
--- /dev/null
+++ b/tests/src/com/android/contacts/widget/CompositeListAdapterTest.java
@@ -0,0 +1,324 @@
+/*
+ * 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.widget;
+
+import com.google.android.collect.Lists;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.test.AndroidTestCase;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Tests for {@link CompositeListAdapter}.
+ */
+public class CompositeListAdapterTest extends AndroidTestCase {
+
+ private final class MockAdapter extends ArrayAdapter<String> {
+ boolean allItemsEnabled = true;
+ HashSet<Integer> enabledItems = new HashSet<Integer>();
+ int viewTypeCount = 1;
+ HashMap<Integer, Integer> viewTypes = new HashMap<Integer, Integer>();
+
+ private MockAdapter(Context context, List<String> objects) {
+ super(context, android.R.layout.simple_list_item_1, objects);
+ for (int i = 0; i < objects.size(); i++) {
+ viewTypes.put(i, 0);
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return new MockView(getContext(), position);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return allItemsEnabled;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return enabledItems.contains(position);
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return viewTypeCount;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return viewTypes.get(position);
+ }
+ }
+
+ private final class MockView extends View {
+ public MockView(Context context, int position) {
+ super(context);
+ setTag(position);
+ }
+ }
+
+ private final class TestDataSetObserver extends DataSetObserver {
+
+ public int changeCount;
+ public int invalidationCount;
+
+ @Override
+ public void onChanged() {
+ changeCount++;
+ }
+
+ @Override
+ public void onInvalidated() {
+ invalidationCount++;
+ }
+ }
+
+ private MockAdapter mAdapter1;
+ private MockAdapter mAdapter2;
+ private MockAdapter mAdapter3;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mAdapter1 = new MockAdapter(getContext(), Lists.newArrayList("A", "B"));
+ mAdapter2 = new MockAdapter(getContext(), new ArrayList<String>());
+ mAdapter3 = new MockAdapter(getContext(), Lists.newArrayList("C", "D", "E"));
+ }
+
+ public void testGetCount() {
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+ adapter.addAdapter(mAdapter2);
+ adapter.addAdapter(mAdapter3);
+
+ assertEquals(5, adapter.getCount());
+ }
+
+ public void testGetCountWithInvalidation() {
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ assertEquals(0, adapter.getCount());
+
+ adapter.addAdapter(mAdapter1);
+ assertEquals(2, adapter.getCount());
+
+ adapter.addAdapter(mAdapter2);
+ assertEquals(2, adapter.getCount());
+
+ adapter.addAdapter(mAdapter3);
+ assertEquals(5, adapter.getCount());
+ }
+
+ public void testGetItem() {
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+ adapter.addAdapter(mAdapter2);
+ adapter.addAdapter(mAdapter3);
+
+ assertEquals("A", adapter.getItem(0));
+ assertEquals("B", adapter.getItem(1));
+ assertEquals("C", adapter.getItem(2));
+ assertEquals("D", adapter.getItem(3));
+ assertEquals("E", adapter.getItem(4));
+ }
+
+ public void testGetItemId() {
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+ adapter.addAdapter(mAdapter2);
+ adapter.addAdapter(mAdapter3);
+
+ assertEquals(0, adapter.getItemId(0));
+ assertEquals(1, adapter.getItemId(1));
+ assertEquals(0, adapter.getItemId(2));
+ assertEquals(1, adapter.getItemId(3));
+ assertEquals(2, adapter.getItemId(4));
+ }
+
+ public void testGetView() {
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+ adapter.addAdapter(mAdapter2);
+ adapter.addAdapter(mAdapter3);
+
+ assertEquals(0, adapter.getView(0, null, null).getTag());
+ assertEquals(1, adapter.getView(1, null, null).getTag());
+ assertEquals(0, adapter.getView(2, null, null).getTag());
+ assertEquals(1, adapter.getView(3, null, null).getTag());
+ assertEquals(2, adapter.getView(4, null, null).getTag());
+ }
+
+ public void testGetViewTypeCount() {
+ mAdapter1.viewTypeCount = 2;
+ mAdapter2.viewTypeCount = 3;
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+ adapter.addAdapter(mAdapter2);
+ adapter.addAdapter(mAdapter3);
+
+ // Note that mAdapter2 adds an implicit +1
+ assertEquals(6, adapter.getViewTypeCount());
+ }
+
+ public void testGetItemViewType() {
+ mAdapter1.viewTypeCount = 2;
+ mAdapter1.viewTypes.put(0, 1);
+ mAdapter1.viewTypes.put(1, 0);
+
+ mAdapter3.viewTypeCount = 3;
+ mAdapter3.viewTypes.put(0, 1);
+ mAdapter3.viewTypes.put(1, 2);
+ mAdapter3.viewTypes.put(2, 0);
+
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+ adapter.addAdapter(mAdapter2);
+ adapter.addAdapter(mAdapter3);
+
+ assertEquals(1, adapter.getItemViewType(0));
+ assertEquals(0, adapter.getItemViewType(1));
+
+ // Note: mAdapter2 throws in a +1
+
+ assertEquals(4, adapter.getItemViewType(2));
+ assertEquals(5, adapter.getItemViewType(3));
+ assertEquals(3, adapter.getItemViewType(4));
+ }
+
+ public void testNotifyDataSetChangedPropagated() {
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+ adapter.addAdapter(mAdapter2);
+
+ TestDataSetObserver observer = new TestDataSetObserver();
+ adapter.registerDataSetObserver(observer);
+ mAdapter1.add("X");
+
+ assertEquals(1, observer.changeCount);
+ assertEquals(0, observer.invalidationCount);
+ assertEquals(3, adapter.getCount());
+ assertEquals("A", adapter.getItem(0));
+ assertEquals("B", adapter.getItem(1));
+ assertEquals("X", adapter.getItem(2));
+
+ mAdapter2.add("Y");
+ assertEquals(2, observer.changeCount);
+ assertEquals(0, observer.invalidationCount);
+ assertEquals(4, adapter.getCount());
+ assertEquals("A", adapter.getItem(0));
+ assertEquals("B", adapter.getItem(1));
+ assertEquals("X", adapter.getItem(2));
+ assertEquals("Y", adapter.getItem(3));
+
+ }
+
+ public void testNotifyDataSetChangedOnAddingAdapter() {
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+
+ TestDataSetObserver observer = new TestDataSetObserver();
+ adapter.registerDataSetObserver(observer);
+ adapter.addAdapter(mAdapter3);
+
+ assertEquals(1, observer.changeCount);
+ assertEquals(0, observer.invalidationCount);
+ assertEquals(5, adapter.getCount());
+ assertEquals("A", adapter.getItem(0));
+ assertEquals("B", adapter.getItem(1));
+ assertEquals("C", adapter.getItem(2));
+ assertEquals("D", adapter.getItem(3));
+ assertEquals("E", adapter.getItem(4));
+ }
+
+ public void testNotifyDataSetInvalidated() {
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+
+ TestDataSetObserver observer = new TestDataSetObserver();
+ adapter.registerDataSetObserver(observer);
+
+ mAdapter1.remove("A");
+ assertEquals(1, observer.changeCount);
+ assertEquals(0, observer.invalidationCount);
+ assertEquals(1, adapter.getCount());
+
+ mAdapter1.remove("B");
+ assertEquals(1, observer.changeCount);
+ assertEquals(1, observer.invalidationCount);
+ assertEquals(0, adapter.getCount());
+ }
+
+ public void testAreAllItemsEnabled() {
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+ adapter.addAdapter(mAdapter3);
+
+ assertTrue(adapter.areAllItemsEnabled());
+ }
+
+ public void testAreAllItemsEnabledWithInvalidation() {
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+ assertTrue(adapter.areAllItemsEnabled());
+
+ mAdapter3.allItemsEnabled = false;
+ adapter.addAdapter(mAdapter3);
+
+ assertFalse(adapter.areAllItemsEnabled());
+ }
+
+ public void testIsEnabled() {
+ mAdapter1.allItemsEnabled = false;
+ mAdapter1.enabledItems.add(1);
+
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+ adapter.addAdapter(mAdapter2);
+ adapter.addAdapter(mAdapter3);
+
+ assertFalse(adapter.isEnabled(0));
+ assertTrue(adapter.isEnabled(1));
+ assertTrue(adapter.isEnabled(2));
+ assertTrue(adapter.isEnabled(3));
+ assertTrue(adapter.isEnabled(4));
+ }
+
+ public void testIsEnabledWhenAllEnabledAtLeastOneAdapter() {
+ mAdapter1.allItemsEnabled = false;
+ mAdapter1.enabledItems.add(1);
+ mAdapter3.allItemsEnabled = false;
+ mAdapter3.enabledItems.add(1);
+
+ CompositeListAdapter adapter = new CompositeListAdapter();
+ adapter.addAdapter(mAdapter1);
+ adapter.addAdapter(mAdapter3);
+
+ assertFalse(adapter.isEnabled(0));
+ assertTrue(adapter.isEnabled(1));
+ assertFalse(adapter.isEnabled(2));
+ assertTrue(adapter.isEnabled(3));
+ assertFalse(adapter.isEnabled(4));
+ }
+}