Merge "Import translations. DO NOT MERGE"
diff --git a/res/drawable-hdpi/ic_menu_remove_field_holo_light.png b/res/drawable-hdpi/ic_menu_remove_field_holo_light.png
new file mode 100644
index 0000000..03fd2fb
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_text_holo_light.png b/res/drawable-hdpi/ic_text_holo_light.png
new file mode 100644
index 0000000..01af189
--- /dev/null
+++ b/res/drawable-hdpi/ic_text_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_remove_field_holo_light.png b/res/drawable-mdpi/ic_menu_remove_field_holo_light.png
new file mode 100644
index 0000000..8c44e70
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_text_holo_light.png b/res/drawable-mdpi/ic_text_holo_light.png
new file mode 100644
index 0000000..76dae05
--- /dev/null
+++ b/res/drawable-mdpi/ic_text_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png b/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png
new file mode 100644
index 0000000..65a6b7b
--- /dev/null
+++ b/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_text_holo_light.png b/res/drawable-xhdpi/ic_text_holo_light.png
new file mode 100644
index 0000000..6fb8e92
--- /dev/null
+++ b/res/drawable-xhdpi/ic_text_holo_light.png
Binary files differ
diff --git a/res/layout-sw580dp/text_fields_editor_view.xml b/res/layout-sw580dp/text_fields_editor_view.xml
new file mode 100644
index 0000000..89970c6
--- /dev/null
+++ b/res/layout-sw580dp/text_fields_editor_view.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<com.android.contacts.editor.TextFieldsEditorView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="horizontal"
+        android:gravity="center_vertical"
+        android:focusable="true"
+        android:clickable="true">
+
+        <include
+            android:id="@+id/editors"
+            layout="@layout/edit_field_list" />
+
+        <include
+            android:id="@+id/expansion_view_container"
+            layout="@layout/edit_expansion_view"
+            android:visibility="gone" />
+
+        <include
+            android:id="@+id/spinner"
+            layout="@layout/edit_spinner"
+            android:visibility="gone" />
+
+        <include
+            android:id="@+id/delete_button_container"
+            layout="@layout/edit_delete_button"
+            android:visibility="gone" />
+
+    </LinearLayout>
+
+</com.android.contacts.editor.TextFieldsEditorView>
diff --git a/res/layout/edit_date_picker.xml b/res/layout/edit_date_picker.xml
new file mode 100644
index 0000000..d951652
--- /dev/null
+++ b/res/layout/edit_date_picker.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<!-- Button to select a date in the contact editor. -->
+
+<Button
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/date_view"
+    style="?android:attr/spinnerStyle"
+    android:layout_width="0dip"
+    android:layout_height="@dimen/editor_min_line_item_height"
+    android:layout_weight="1"
+    android:gravity="center_vertical"
+    android:layout_marginLeft="@dimen/editor_field_left_padding"
+    android:layout_marginRight="@dimen/editor_field_right_padding"
+    android:textAppearance="?android:attr/textAppearanceMedium"
+    android:paddingLeft="12dip" />
diff --git a/res/layout/edit_delete_button.xml b/res/layout/edit_delete_button.xml
new file mode 100644
index 0000000..ca9d8b8
--- /dev/null
+++ b/res/layout/edit_delete_button.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<!-- "Delete field" button in the contact editor. -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="@dimen/editor_min_line_item_height"
+    android:layout_marginRight="2dip"
+    android:layout_gravity="bottom">
+    <ImageView
+        android:id="@+id/delete_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:duplicateParentState="true"
+        android:background="?android:attr/selectableItemBackground"
+        android:src="@drawable/ic_menu_remove_field_holo_light"
+        android:paddingLeft="@dimen/editor_round_button_padding_left"
+        android:paddingRight="@dimen/editor_round_button_padding_right"
+        android:paddingTop="@dimen/editor_round_button_padding_top"
+        android:paddingBottom="@dimen/editor_round_button_padding_bottom"
+        android:contentDescription="@string/description_minus_button" />
+</FrameLayout>
diff --git a/res/layout/edit_expansion_view.xml b/res/layout/edit_expansion_view.xml
new file mode 100644
index 0000000..f196a69
--- /dev/null
+++ b/res/layout/edit_expansion_view.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<!-- "More" or "less" expansion button in the contact editor. -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="@dimen/editor_min_line_item_height"
+    android:layout_gravity="top">
+    <ImageView
+        android:id="@+id/expansion_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:duplicateParentState="true"
+        android:background="?android:attr/selectableItemBackground"
+        android:paddingLeft="@dimen/editor_round_button_padding_left"
+        android:paddingRight="@dimen/editor_round_button_padding_right"
+        android:paddingTop="@dimen/editor_round_button_padding_top"
+        android:paddingBottom="@dimen/editor_round_button_padding_bottom" />
+</FrameLayout>
diff --git a/res/layout/edit_field_list.xml b/res/layout/edit_field_list.xml
new file mode 100644
index 0000000..354ea65
--- /dev/null
+++ b/res/layout/edit_field_list.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<!-- Layout to contain a list of fields in the contact editor. -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/editors"
+    android:layout_width="0dip"
+    android:layout_weight="1"
+    android:layout_height="wrap_content"
+    android:paddingLeft="@dimen/editor_field_left_padding"
+    android:orientation="vertical" />
diff --git a/res/layout/edit_field_list_with_anchor_view.xml b/res/layout/edit_field_list_with_anchor_view.xml
new file mode 100644
index 0000000..493226e
--- /dev/null
+++ b/res/layout/edit_field_list_with_anchor_view.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<!-- Layout that behaves similarly to edit_field_list.xml,
+     but also has an anchor view for ListPopupWindow -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="0dip"
+    android:layout_height="wrap_content"
+    android:layout_weight="1"
+    android:paddingLeft="@dimen/editor_field_left_padding"
+    android:orientation="vertical">
+    <LinearLayout
+         android:id="@+id/editors"
+         android:layout_width="match_parent"
+         android:layout_height="wrap_content"
+         android:orientation="vertical" />
+    <View
+         android:id="@+id/anchor_view"
+         android:layout_width="match_parent"
+         android:layout_height="0px" />
+</LinearLayout>
diff --git a/res/layout/edit_spinner.xml b/res/layout/edit_spinner.xml
new file mode 100644
index 0000000..43ac624
--- /dev/null
+++ b/res/layout/edit_spinner.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<!-- Spinner for a field in the contact editor. -->
+
+<!-- Note: explicitly override the default left and right padding on spinner -->
+<Spinner
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/spinner"
+    android:layout_gravity="bottom"
+    android:layout_width="@dimen/editor_type_label_width"
+    android:layout_height="@dimen/editor_min_line_item_height"
+    android:paddingLeft="0dip"
+    android:paddingRight="10dip"/>
diff --git a/res/layout/event_field_editor_view.xml b/res/layout/event_field_editor_view.xml
new file mode 100644
index 0000000..560b9e1
--- /dev/null
+++ b/res/layout/event_field_editor_view.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- Editor for a single event entry in the contact editor -->
+
+<com.android.contacts.editor.EventFieldEditorView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/editor_min_line_item_height"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="horizontal"
+        android:gravity="center_vertical"
+        android:focusable="true"
+        android:clickable="true">
+
+        <include
+            android:id="@+id/date_view"
+            layout="@layout/edit_date_picker" />
+
+        <Spinner
+            android:id="@+id/spinner"
+            android:layout_width="@dimen/editor_type_label_width"
+            android:layout_height="match_parent"
+            android:layout_gravity="bottom"
+            android:paddingLeft="0dip"
+            android:paddingRight="10dip"
+            android:visibility="gone"/>
+
+        <include
+            android:id="@+id/delete_button_container"
+            layout="@layout/edit_delete_button"
+            android:visibility="gone" />
+
+    </LinearLayout>
+
+</com.android.contacts.editor.EventFieldEditorView>
diff --git a/res/layout/name_edit_expansion_view.xml b/res/layout/name_edit_expansion_view.xml
new file mode 100644
index 0000000..44c1317
--- /dev/null
+++ b/res/layout/name_edit_expansion_view.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<!-- "More" or "less" expansion button in the contact editor. -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="@dimen/editor_min_line_item_height"
+    android:layout_gravity="top"
+    android:contentDescription="@string/expand_collapse_name_fields_description"
+    android:importantForAccessibility="yes">
+    <ImageView
+        android:id="@+id/expansion_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:duplicateParentState="true"
+        android:background="?android:attr/selectableItemBackground"
+        android:paddingLeft="@dimen/editor_round_button_padding_left"
+        android:paddingRight="@dimen/editor_round_button_padding_right"
+        android:paddingTop="@dimen/editor_round_button_padding_top"
+        android:paddingBottom="@dimen/editor_round_button_padding_bottom" />
+</FrameLayout>
diff --git a/res/layout/phonetic_name_editor_view.xml b/res/layout/phonetic_name_editor_view.xml
new file mode 100644
index 0000000..c0e8827
--- /dev/null
+++ b/res/layout/phonetic_name_editor_view.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<com.android.contacts.editor.PhoneticNameEditorView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="@dimen/editor_min_line_item_height"
+    android:orientation="vertical">
+
+    <include
+        android:id="@+id/spinner"
+        layout="@layout/edit_spinner"
+        android:visibility="gone" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:gravity="center_vertical"
+        android:focusable="true"
+        android:clickable="true">
+
+        <include
+            android:id="@+id/editors"
+            layout="@layout/edit_field_list" />
+
+        <include
+            android:id="@+id/expansion_view_container"
+            layout="@layout/name_edit_expansion_view"
+            android:visibility="gone" />
+
+        <include
+            android:id="@+id/delete_button_container"
+            layout="@layout/edit_delete_button"
+            android:visibility="gone" />
+
+    </LinearLayout>
+
+</com.android.contacts.editor.PhoneticNameEditorView>
diff --git a/res/layout/structured_name_editor_view.xml b/res/layout/structured_name_editor_view.xml
new file mode 100644
index 0000000..4fa5ae1
--- /dev/null
+++ b/res/layout/structured_name_editor_view.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<com.android.contacts.editor.StructuredNameEditorView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="@dimen/editor_min_line_item_height"
+    android:orientation="vertical">
+
+    <include
+        android:id="@+id/spinner"
+        layout="@layout/edit_spinner"
+        android:visibility="gone" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:focusable="true"
+        android:clickable="true">
+
+        <include
+            layout="@layout/edit_field_list_with_anchor_view" />
+
+        <include
+            android:id="@+id/expansion_view_container"
+            layout="@layout/name_edit_expansion_view"
+            android:visibility="gone" />
+
+        <include
+            android:id="@+id/delete_button_container"
+            layout="@layout/edit_delete_button"
+            android:visibility="gone" />
+
+    </LinearLayout>
+
+</com.android.contacts.editor.StructuredNameEditorView>
diff --git a/res/layout/text_fields_editor_view.xml b/res/layout/text_fields_editor_view.xml
new file mode 100644
index 0000000..6572e4c
--- /dev/null
+++ b/res/layout/text_fields_editor_view.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<com.android.contacts.editor.TextFieldsEditorView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="horizontal"
+        android:gravity="center_vertical"
+        android:focusable="true"
+        android:clickable="true">
+
+        <include
+            layout="@layout/edit_field_list_with_anchor_view" />
+
+        <include
+            android:id="@+id/expansion_view_container"
+            layout="@layout/edit_expansion_view"
+            android:visibility="gone" />
+
+        <include
+            android:id="@+id/spinner"
+            layout="@layout/edit_spinner"
+            android:visibility="gone" />
+
+        <include
+            android:id="@+id/delete_button_container"
+            layout="@layout/edit_delete_button"
+            android:visibility="gone" />
+
+    </LinearLayout>
+
+</com.android.contacts.editor.TextFieldsEditorView>
diff --git a/res/mipmap-hdpi/ic_launcher_contacts.png b/res/mipmap-hdpi/ic_launcher_contacts.png
new file mode 100644
index 0000000..e0136f6
--- /dev/null
+++ b/res/mipmap-hdpi/ic_launcher_contacts.png
Binary files differ
diff --git a/res/mipmap-mdpi/ic_launcher_contacts.png b/res/mipmap-mdpi/ic_launcher_contacts.png
new file mode 100644
index 0000000..3d490c3
--- /dev/null
+++ b/res/mipmap-mdpi/ic_launcher_contacts.png
Binary files differ
diff --git a/res/mipmap-xhdpi/ic_launcher_contacts.png b/res/mipmap-xhdpi/ic_launcher_contacts.png
new file mode 100644
index 0000000..dde3cbb
--- /dev/null
+++ b/res/mipmap-xhdpi/ic_launcher_contacts.png
Binary files differ
diff --git a/res/mipmap-xxhdpi/ic_launcher_contacts.png b/res/mipmap-xxhdpi/ic_launcher_contacts.png
new file mode 100644
index 0000000..99b403b
--- /dev/null
+++ b/res/mipmap-xxhdpi/ic_launcher_contacts.png
Binary files differ
diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml
new file mode 100644
index 0000000..50cb55c
--- /dev/null
+++ b/res/values-land/dimens.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<resources>
+    <dimen name="editor_type_label_width">120dip</dimen>
+</resources>
diff --git a/res/values-sw580dp/dimens.xml b/res/values-sw580dp/dimens.xml
index bdbc0d4..4561824 100644
--- a/res/values-sw580dp/dimens.xml
+++ b/res/values-sw580dp/dimens.xml
@@ -16,4 +16,9 @@
 
 <resources>
     <dimen name="detail_item_side_margin">0dip</dimen>
+
+    <dimen name="editor_round_button_padding_left">16dip</dimen>
+    <dimen name="editor_round_button_padding_right">16dip</dimen>
+
+    <dimen name="editor_type_label_width">122dip</dimen>
 </resources>
diff --git a/res/values-sw680dp/dimens.xml b/res/values-sw680dp/dimens.xml
new file mode 100644
index 0000000..b99d0c2
--- /dev/null
+++ b/res/values-sw680dp/dimens.xml
@@ -0,0 +1,22 @@
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<resources>
+    <dimen name="editor_round_button_padding_left">8dip</dimen>
+    <dimen name="editor_round_button_padding_right">8dip</dimen>
+
+    <dimen name="editor_type_label_width">180dip</dimen>
+</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 97e6fe7..8901aea 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -40,4 +40,22 @@
 
     <!-- Top padding of the ListView in the contact tile list -->
     <dimen name="contact_tile_list_padding_top">0dip</dimen>
+
+    <!-- Minimum height of a row in the Editor -->
+    <dimen name="editor_min_line_item_height">48dip</dimen>
+
+    <!-- Padding of the rounded plus/minus/expand/collapse buttons in the editor  -->
+    <dimen name="editor_round_button_padding_left">8dip</dimen>
+    <dimen name="editor_round_button_padding_right">8dip</dimen>
+    <dimen name="editor_round_button_padding_top">8dip</dimen>
+    <dimen name="editor_round_button_padding_bottom">8dip</dimen>
+
+    <!-- Right padding of a field in the Editor -->
+    <dimen name="editor_field_right_padding">4dip</dimen>
+
+    <!-- Left padding of a field in the Editor -->
+    <dimen name="editor_field_left_padding">4dip</dimen>
+
+    <!-- Width of the Type-Label in the Editor -->
+    <dimen name="editor_type_label_width">100dip</dimen>
 </resources>
diff --git a/res/values/donottranslate_config.xml b/res/values/donottranslate_config.xml
index 8603bb7..f4bf5fe 100644
--- a/res/values/donottranslate_config.xml
+++ b/res/values/donottranslate_config.xml
@@ -27,4 +27,7 @@
 
     <!-- If true, the default sort order is primary (i.e. by given name) -->
     <bool name="config_default_display_order_primary">true</bool>
+
+    <!-- If true, the order of name fields in the editor is primary (i.e. given name first) -->
+    <bool name="config_editor_field_order_primary">true</bool>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index cc85566..fc966ee 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -194,4 +194,176 @@
 
     <!-- Contact list filter selection indicating that the list shows all contacts with phone numbers [CHAR LIMIT=64] -->
     <string name="list_filter_phones">All contacts with phone numbers</string>
+
+    <!-- Button to view the updates from the current group on the group detail page [CHAR LIMIT=25] -->
+    <string name="view_updates_from_group">View updates</string>
+
+
+    <!-- Title for data source when creating or editing a contact that doesn't
+         belong to a specific account.  This contact will only exist on the phone
+         and will not be synced. -->
+    <string name="account_phone" product="tablet">Tablet-only, unsynced</string>
+    <!-- Title for data source when creating or editing a contact that doesn't
+         belong to a specific account.  This contact will only exist on the phone
+         and will not be synced. -->
+    <string name="account_phone" product="default">Phone-only, unsynced</string>
+
+    <!-- Header that expands to list all name types when editing a structured name of a contact
+         [CHAR LIMIT=20] -->
+    <string name="nameLabelsGroup">Name</string>
+
+    <!-- Header that expands to list all nickname types when editing a nickname of a contact
+         [CHAR LIMIT=20] -->
+    <string name="nicknameLabelsGroup">Nickname</string>
+
+    <!-- Field title for the full name of a contact [CHAR LIMIT=64]-->
+    <string name="full_name">Name</string>
+    <!-- Field title for the given name of a contact -->
+    <string name="name_given">Given name</string>
+    <!-- Field title for the family name of a contact -->
+    <string name="name_family">Family name</string>
+    <!-- Field title for the prefix name of a contact -->
+    <string name="name_prefix">Name prefix</string>
+    <!-- Field title for the middle name of a contact -->
+    <string name="name_middle">Middle name</string>
+    <!-- Field title for the suffix name of a contact -->
+    <string name="name_suffix">Name suffix</string>
+
+    <!-- Field title for the phonetic name of a contact [CHAR LIMIT=64]-->
+    <string name="name_phonetic">Phonetic name</string>
+
+    <!-- Field title for the phonetic given name of a contact -->
+    <string name="name_phonetic_given">Phonetic given name</string>
+    <!-- Field title for the phonetic middle name of a contact -->
+    <string name="name_phonetic_middle">Phonetic middle name</string>
+    <!-- Field title for the phonetic family name of a contact -->
+    <string name="name_phonetic_family">Phonetic family name</string>
+
+    <!-- Header that expands to list all of the types of phone numbers when editing or creating a
+         phone number for a contact [CHAR LIMIT=20] -->
+    <string name="phoneLabelsGroup">Phone</string>
+
+    <!-- Header that expands to list all of the types of email addresses when editing or creating
+         an email address for a contact [CHAR LIMIT=20] -->
+    <string name="emailLabelsGroup">Email</string>
+
+    <!-- Header that expands to list all of the types of postal addresses when editing or creating
+         an postal address for a contact [CHAR LIMIT=20] -->
+    <string name="postalLabelsGroup">Address</string>
+
+    <!-- Header that expands to list all of the types of IM account when editing or creating an IM
+         account for a contact [CHAR LIMIT=20] -->
+    <string name="imLabelsGroup">IM</string>
+
+    <!-- Header that expands to list all organization types when editing an organization of a
+         contact [CHAR LIMIT=20] -->
+    <string name="organizationLabelsGroup">Organization</string>
+
+    <!-- Header for the list of all relationships for a contact [CHAR LIMIT=20] -->
+    <string name="relationLabelsGroup">Relationship</string>
+
+    <!-- Header that expands to list all event types when editing an event of a contact
+         [CHAR LIMIT=20] -->
+    <string name="eventLabelsGroup">Events</string>
+
+    <!-- Generic action string for text messaging a contact. Used by AccessibilityService to
+         announce the purpose of the view. [CHAR LIMIT=NONE] -->
+    <string name="sms">Text message</string>
+
+    <!-- Field title for the full postal address of a contact [CHAR LIMIT=64]-->
+    <string name="postal_address">Address</string>
+
+    <!-- Hint text for the organization name when editing -->
+    <string name="ghostData_company">Company</string>
+
+    <!-- Hint text for the organization title when editing -->
+    <string name="ghostData_title">Title</string>
+
+    <!-- The label describing the Notes field of a contact. This field allows free form text entry
+         about a contact -->
+    <string name="label_notes">Notes</string>
+
+    <!-- The label describing the SIP address field of a contact. [CHAR LIMIT=20] -->
+    <string name="label_sip_address">Internet call</string>
+
+    <!-- Header that expands to list all website types when editing a website of a contact
+         [CHAR LIMIT=20] -->
+    <string name="websiteLabelsGroup">Website</string>
+
+    <!-- Header for the list of all groups for a contact [CHAR LIMIT=20] -->
+    <string name="groupsLabel">Groups</string>
+
+    <!-- Action string for sending an email to a home email address -->
+    <string name="email_home">Email home</string>
+    <!-- Action string for sending an email to a mobile email address -->
+    <string name="email_mobile">Email mobile</string>
+    <!-- Action string for sending an email to a work email address -->
+    <string name="email_work">Email work</string>
+    <!-- Action string for sending an email to an other email address -->
+    <string name="email_other">Email</string>
+    <!-- Action string for sending an email to a custom email address -->
+    <string name="email_custom">Email <xliff:g id="custom">%s</xliff:g></string>
+
+    <!-- Generic action string for sending an email -->
+    <string name="email">Email</string>
+
+    <!-- Field title for the street of a structured postal address of a contact -->
+    <string name="postal_street">Street</string>
+    <!-- Field title for the PO box of a structured postal address of a contact -->
+    <string name="postal_pobox">PO box</string>
+    <!-- Field title for the neighborhood of a structured postal address of a contact -->
+    <string name="postal_neighborhood">Neighborhood</string>
+    <!-- Field title for the city of a structured postal address of a contact -->
+    <string name="postal_city">City</string>
+    <!-- Field title for the region, or state, of a structured postal address of a contact -->
+    <string name="postal_region">State</string>
+    <!-- Field title for the postal code of a structured postal address of a contact -->
+    <string name="postal_postcode">ZIP code</string>
+    <!-- Field title for the country of a structured postal address of a contact -->
+    <string name="postal_country">Country</string>
+
+    <!-- Action string for viewing a home postal address -->
+    <string name="map_home">View home address</string>
+    <!-- Action string for viewing a work postal address -->
+    <string name="map_work">View work address</string>
+    <!-- Action string for viewing an other postal address -->
+    <string name="map_other">View address</string>
+    <!-- Action string for viewing a custom postal address -->
+    <string name="map_custom">View <xliff:g id="custom">%s</xliff:g> address</string>
+
+    <!-- Action string for starting an IM chat with the AIM protocol -->
+    <string name="chat_aim">Chat using AIM</string>
+    <!-- Action string for starting an IM chat with the MSN or Windows Live protocol -->
+    <string name="chat_msn">Chat using Windows Live</string>
+    <!-- Action string for starting an IM chat with the Yahoo protocol -->
+    <string name="chat_yahoo">Chat using Yahoo</string>
+    <!-- Action string for starting an IM chat with the Skype protocol -->
+    <string name="chat_skype">Chat using Skype</string>
+    <!-- Action string for starting an IM chat with the QQ protocol -->
+    <string name="chat_qq">Chat using QQ</string>
+    <!-- Action string for starting an IM chat with the Google Talk protocol -->
+    <string name="chat_gtalk">Chat using Google Talk</string>
+    <!-- Action string for starting an IM chat with the ICQ protocol -->
+    <string name="chat_icq">Chat using ICQ</string>
+    <!-- Action string for starting an IM chat with the Jabber protocol -->
+    <string name="chat_jabber">Chat using Jabber</string>
+
+    <!-- Generic action string for starting an IM chat -->
+    <string name="chat">Chat</string>
+
+    <!-- String describing the Contact Editor Minus button
+
+         Used by AccessibilityService to announce the purpose of the button.
+
+         [CHAR LIMIT=NONE]
+    -->
+    <string name="description_minus_button">delete</string>
+
+    <!-- Content description for the expand or collapse name fields button.
+         Clicking this button causes the name editor to toggle between showing
+         a single field where the entire name is edited at once, or multiple
+         fields corresponding to each part of the name (Name Prefix, First Name,
+         Middle Name, Last Name, Name Suffix).
+         [CHAR LIMIT=NONE] -->
+    <string name="expand_collapse_name_fields_description">Expand or collapse name fields</string>
 </resources>
diff --git a/src/com/android/contacts/common/model/account/AccountType.java b/src/com/android/contacts/common/model/account/AccountType.java
new file mode 100644
index 0000000..cfafa79
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/AccountType.java
@@ -0,0 +1,531 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Internal structure that represents constraints and styles for a specific data
+ * source, such as the various data types they support, including details on how
+ * those types should be rendered and edited.
+ * <p>
+ * In the future this may be inflated from XML defined by a data source.
+ */
+public abstract class AccountType {
+    private static final String TAG = "AccountType";
+
+    /**
+     * The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to.
+     */
+    public String accountType = null;
+
+    /**
+     * The {@link RawContacts#DATA_SET} these constraints apply to.
+     */
+    public String dataSet = null;
+
+    /**
+     * Package that resources should be loaded from.  Will be null for embedded types, in which
+     * case resources are stored in this package itself.
+     *
+     * TODO Clean up {@link #resourcePackageName}, {@link #syncAdapterPackageName} and
+     * {@link #getViewContactNotifyServicePackageName()}.
+     *
+     * There's the following invariants:
+     * - {@link #syncAdapterPackageName} is always set to the actual sync adapter package name.
+     * - {@link #resourcePackageName} too is set to the same value, unless {@link #isEmbedded()},
+     *   in which case it'll be null.
+     * There's an unfortunate exception of {@link FallbackAccountType}.  Even though it
+     * {@link #isEmbedded()}, but we set non-null to {@link #resourcePackageName} for unit tests.
+     */
+    public String resourcePackageName;
+    /**
+     * The package name for the authenticator (for the embedded types, i.e. Google and Exchange)
+     * or the sync adapter (for external type, including extensions).
+     */
+    public String syncAdapterPackageName;
+
+    public int titleRes;
+    public int iconRes;
+
+    /**
+     * Set of {@link DataKind} supported by this source.
+     */
+    private ArrayList<DataKind> mKinds = Lists.newArrayList();
+
+    /**
+     * Lookup map of {@link #mKinds} on {@link DataKind#mimeType}.
+     */
+    private HashMap<String, DataKind> mMimeKinds = Maps.newHashMap();
+
+    protected boolean mIsInitialized;
+
+    protected static class DefinitionException extends Exception {
+        public DefinitionException(String message) {
+            super(message);
+        }
+
+        public DefinitionException(String message, Exception inner) {
+            super(message, inner);
+        }
+    }
+
+    /**
+     * Whether this account type was able to be fully initialized.  This may be false if
+     * (for example) the package name associated with the account type could not be found.
+     */
+    public final boolean isInitialized() {
+        return mIsInitialized;
+    }
+
+    /**
+     * @return Whether this type is an "embedded" type.  i.e. any of {@link FallbackAccountType},
+     * {@link GoogleAccountType} or {@link ExternalAccountType}.
+     *
+     * If an embedded type cannot be initialized (i.e. if {@link #isInitialized()} returns
+     * {@code false}) it's considered critical, and the application will crash.  On the other
+     * hand if it's not an embedded type, we just skip loading the type.
+     */
+    public boolean isEmbedded() {
+        return true;
+    }
+
+    public boolean isExtension() {
+        return false;
+    }
+
+    /**
+     * @return True if contacts can be created and edited using this app. If false,
+     * there could still be an external editor as provided by
+     * {@link #getEditContactActivityClassName()} or {@link #getCreateContactActivityClassName()}
+     */
+    public abstract boolean areContactsWritable();
+
+    /**
+     * Returns an optional custom edit activity.
+     *
+     * Only makes sense for non-embedded account types.
+     * The activity class should reside in the sync adapter package as determined by
+     * {@link #syncAdapterPackageName}.
+     */
+    public String getEditContactActivityClassName() {
+        return null;
+    }
+
+    /**
+     * Returns an optional custom new contact activity.
+     *
+     * Only makes sense for non-embedded account types.
+     * The activity class should reside in the sync adapter package as determined by
+     * {@link #syncAdapterPackageName}.
+     */
+    public String getCreateContactActivityClassName() {
+        return null;
+    }
+
+    /**
+     * Returns an optional custom invite contact activity.
+     *
+     * Only makes sense for non-embedded account types.
+     * The activity class should reside in the sync adapter package as determined by
+     * {@link #syncAdapterPackageName}.
+     */
+    public String getInviteContactActivityClassName() {
+        return null;
+    }
+
+    /**
+     * Returns an optional service that can be launched whenever a contact is being looked at.
+     * This allows the sync adapter to provide more up-to-date information.
+     *
+     * The service class should reside in the sync adapter package as determined by
+     * {@link #getViewContactNotifyServicePackageName()}.
+     */
+    public String getViewContactNotifyServiceClassName() {
+        return null;
+    }
+
+    /**
+     * TODO This is way too hacky should be removed.
+     *
+     * This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName}
+     * is the authenticator package name but the notification service is in the sync adapter
+     * package.  See {@link #resourcePackageName} -- we should clean up those.
+     */
+    public String getViewContactNotifyServicePackageName() {
+        return syncAdapterPackageName;
+    }
+
+    /** Returns an optional Activity string that can be used to view the group. */
+    public String getViewGroupActivity() {
+        return null;
+    }
+
+    /** Returns an optional Activity string that can be used to view the stream item. */
+    public String getViewStreamItemActivity() {
+        return null;
+    }
+
+    /** Returns an optional Activity string that can be used to view the stream item photo. */
+    public String getViewStreamItemPhotoActivity() {
+        return null;
+    }
+
+    public CharSequence getDisplayLabel(Context context) {
+        // Note this resource is defined in the sync adapter package, not resourcePackageName.
+        return getResourceText(context, syncAdapterPackageName, titleRes, accountType);
+    }
+
+    /**
+     * @return resource ID for the "invite contact" action label, or -1 if not defined.
+     */
+    protected int getInviteContactActionResId() {
+        return -1;
+    }
+
+    /**
+     * @return resource ID for the "view group" label, or -1 if not defined.
+     */
+    protected int getViewGroupLabelResId() {
+        return -1;
+    }
+
+    /**
+     * Returns {@link AccountTypeWithDataSet} for this type.
+     */
+    public AccountTypeWithDataSet getAccountTypeAndDataSet() {
+        return AccountTypeWithDataSet.get(accountType, dataSet);
+    }
+
+    /**
+     * Returns a list of additional package names that should be inspected as additional
+     * external account types.  This allows for a primary account type to indicate other packages
+     * that may not be sync adapters but which still provide contact data, perhaps under a
+     * separate data set within the account.
+     */
+    public List<String> getExtensionPackageNames() {
+        return new ArrayList<String>();
+    }
+
+    /**
+     * Returns an optional custom label for the "invite contact" action, which will be shown on
+     * the contact card.  (If not defined, returns null.)
+     */
+    public CharSequence getInviteContactActionLabel(Context context) {
+        // Note this resource is defined in the sync adapter package, not resourcePackageName.
+        return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), "");
+    }
+
+    /**
+     * Returns a label for the "view group" action. If not defined, this falls back to our
+     * own "View Updates" string
+     */
+    public CharSequence getViewGroupLabel(Context context) {
+        // Note this resource is defined in the sync adapter package, not resourcePackageName.
+        final CharSequence customTitle =
+                getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null);
+
+        return customTitle == null
+                ? context.getText(R.string.view_updates_from_group)
+                : customTitle;
+    }
+
+    /**
+     * Return a string resource loaded from the given package (or the current package
+     * if {@code packageName} is null), unless {@code resId} is -1, in which case it returns
+     * {@code defaultValue}.
+     *
+     * (The behavior is undefined if the resource or package doesn't exist.)
+     */
+    @VisibleForTesting
+    static CharSequence getResourceText(Context context, String packageName, int resId,
+            String defaultValue) {
+        if (resId != -1 && packageName != null) {
+            final PackageManager pm = context.getPackageManager();
+            return pm.getText(packageName, resId, null);
+        } else if (resId != -1) {
+            return context.getText(resId);
+        } else {
+            return defaultValue;
+        }
+    }
+
+    public Drawable getDisplayIcon(Context context) {
+        if (this.titleRes != -1 && this.syncAdapterPackageName != null) {
+            final PackageManager pm = context.getPackageManager();
+            return pm.getDrawable(this.syncAdapterPackageName, this.iconRes, null);
+        } else if (this.titleRes != -1) {
+            return context.getResources().getDrawable(this.iconRes);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Whether or not groups created under this account type have editable membership lists.
+     */
+    abstract public boolean isGroupMembershipEditable();
+
+    /**
+     * {@link Comparator} to sort by {@link DataKind#weight}.
+     */
+    private static Comparator<DataKind> sWeightComparator = new Comparator<DataKind>() {
+        @Override
+        public int compare(DataKind object1, DataKind object2) {
+            return object1.weight - object2.weight;
+        }
+    };
+
+    /**
+     * Return list of {@link DataKind} supported, sorted by
+     * {@link DataKind#weight}.
+     */
+    public ArrayList<DataKind> getSortedDataKinds() {
+        // TODO: optimize by marking if already sorted
+        Collections.sort(mKinds, sWeightComparator);
+        return mKinds;
+    }
+
+    /**
+     * Find the {@link DataKind} for a specific MIME-type, if it's handled by
+     * this data source.
+     */
+    public DataKind getKindForMimetype(String mimeType) {
+        return this.mMimeKinds.get(mimeType);
+    }
+
+    /**
+     * Add given {@link DataKind} to list of those provided by this source.
+     */
+    public DataKind addKind(DataKind kind) throws DefinitionException {
+        if (kind.mimeType == null) {
+            throw new DefinitionException("null is not a valid mime type");
+        }
+        if (mMimeKinds.get(kind.mimeType) != null) {
+            throw new DefinitionException(
+                    "mime type '" + kind.mimeType + "' is already registered");
+        }
+
+        kind.resourcePackageName = this.resourcePackageName;
+        this.mKinds.add(kind);
+        this.mMimeKinds.put(kind.mimeType, kind);
+        return kind;
+    }
+
+    /**
+     * Description of a specific "type" or "label" of a {@link DataKind} row,
+     * such as {@link Phone#TYPE_WORK}. Includes constraints on total number of
+     * rows a {@link Contacts} may have of this type, and details on how
+     * user-defined labels are stored.
+     */
+    public static class EditType {
+        public int rawValue;
+        public int labelRes;
+        public boolean secondary;
+        /**
+         * The number of entries allowed for the type. -1 if not specified.
+         * @see DataKind#typeOverallMax
+         */
+        public int specificMax;
+        public String customColumn;
+
+        public EditType(int rawValue, int labelRes) {
+            this.rawValue = rawValue;
+            this.labelRes = labelRes;
+            this.specificMax = -1;
+        }
+
+        public EditType setSecondary(boolean secondary) {
+            this.secondary = secondary;
+            return this;
+        }
+
+        public EditType setSpecificMax(int specificMax) {
+            this.specificMax = specificMax;
+            return this;
+        }
+
+        public EditType setCustomColumn(String customColumn) {
+            this.customColumn = customColumn;
+            return this;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (object instanceof EditType) {
+                final EditType other = (EditType)object;
+                return other.rawValue == rawValue;
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return rawValue;
+        }
+
+        @Override
+        public String toString() {
+            return this.getClass().getSimpleName()
+                    + " rawValue=" + rawValue
+                    + " labelRes=" + labelRes
+                    + " secondary=" + secondary
+                    + " specificMax=" + specificMax
+                    + " customColumn=" + customColumn;
+        }
+    }
+
+    public static class EventEditType extends EditType {
+        private boolean mYearOptional;
+
+        public EventEditType(int rawValue, int labelRes) {
+            super(rawValue, labelRes);
+        }
+
+        public boolean isYearOptional() {
+            return mYearOptional;
+        }
+
+        public EventEditType setYearOptional(boolean yearOptional) {
+            mYearOptional = yearOptional;
+            return this;
+        }
+
+        @Override
+        public String toString() {
+            return super.toString() + " mYearOptional=" + mYearOptional;
+        }
+    }
+
+    /**
+     * Description of a user-editable field on a {@link DataKind} row, such as
+     * {@link Phone#NUMBER}. Includes flags to apply to an {@link EditText}, and
+     * the column where this field is stored.
+     */
+    public static final class EditField {
+        public String column;
+        public int titleRes;
+        public int inputType;
+        public int minLines;
+        public boolean optional;
+        public boolean shortForm;
+        public boolean longForm;
+
+        public EditField(String column, int titleRes) {
+            this.column = column;
+            this.titleRes = titleRes;
+        }
+
+        public EditField(String column, int titleRes, int inputType) {
+            this(column, titleRes);
+            this.inputType = inputType;
+        }
+
+        public EditField setOptional(boolean optional) {
+            this.optional = optional;
+            return this;
+        }
+
+        public EditField setShortForm(boolean shortForm) {
+            this.shortForm = shortForm;
+            return this;
+        }
+
+        public EditField setLongForm(boolean longForm) {
+            this.longForm = longForm;
+            return this;
+        }
+
+        public EditField setMinLines(int minLines) {
+            this.minLines = minLines;
+            return this;
+        }
+
+        public boolean isMultiLine() {
+            return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
+        }
+
+
+        @Override
+        public String toString() {
+            return this.getClass().getSimpleName() + ":"
+                    + " column=" + column
+                    + " titleRes=" + titleRes
+                    + " inputType=" + inputType
+                    + " minLines=" + minLines
+                    + " optional=" + optional
+                    + " shortForm=" + shortForm
+                    + " longForm=" + longForm;
+        }
+    }
+
+    /**
+     * Generic method of inflating a given {@link ContentValues} into a user-readable
+     * {@link CharSequence}. For example, an inflater could combine the multiple
+     * columns of {@link StructuredPostal} together using a string resource
+     * before presenting to the user.
+     */
+    public interface StringInflater {
+        public CharSequence inflateUsing(Context context, ContentValues values);
+    }
+
+    /**
+     * Compare two {@link AccountType} by their {@link AccountType#getDisplayLabel} with the
+     * current locale.
+     */
+    public static class DisplayLabelComparator implements Comparator<AccountType> {
+        private final Context mContext;
+        /** {@link Comparator} for the current locale. */
+        private final Collator mCollator = Collator.getInstance();
+
+        public DisplayLabelComparator(Context context) {
+            mContext = context;
+        }
+
+        private String getDisplayLabel(AccountType type) {
+            CharSequence label = type.getDisplayLabel(mContext);
+            return (label == null) ? "" : label.toString();
+        }
+
+        @Override
+        public int compare(AccountType lhs, AccountType rhs) {
+            return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs));
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/model/account/AccountTypeWithDataSet.java b/src/com/android/contacts/common/model/account/AccountTypeWithDataSet.java
new file mode 100644
index 0000000..f6bcf24
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/AccountTypeWithDataSet.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+
+import com.google.common.base.Objects;
+
+
+/**
+ * Encapsulates an "account type" string and a "data set" string.
+ */
+public class AccountTypeWithDataSet {
+
+    private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID};
+    private static final Uri RAW_CONTACTS_URI_LIMIT_1 = RawContacts.CONTENT_URI.buildUpon()
+            .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1").build();
+
+    /** account type.  Can be null for fallback type. */
+    public final String accountType;
+
+    /** dataSet may be null, but never be "". */
+    public final String dataSet;
+
+    private AccountTypeWithDataSet(String accountType, String dataSet) {
+        this.accountType = TextUtils.isEmpty(accountType) ? null : accountType;
+        this.dataSet = TextUtils.isEmpty(dataSet) ? null : dataSet;
+    }
+
+    public static AccountTypeWithDataSet get(String accountType, String dataSet) {
+        return new AccountTypeWithDataSet(accountType, dataSet);
+    }
+
+    /**
+     * Return true if there are any contacts in the database with this account type and data set.
+     * Touches DB. Don't use in the UI thread.
+     */
+    public boolean hasData(Context context) {
+        final String BASE_SELECTION = RawContacts.ACCOUNT_TYPE + " = ?";
+        final String selection;
+        final String[] args;
+        if (TextUtils.isEmpty(dataSet)) {
+            selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL";
+            args = new String[] {accountType};
+        } else {
+            selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?";
+            args = new String[] {accountType, dataSet};
+        }
+
+        final Cursor c = context.getContentResolver().query(RAW_CONTACTS_URI_LIMIT_1,
+                ID_PROJECTION, selection, args, null);
+        if (c == null) return false;
+        try {
+            return c.moveToFirst();
+        } finally {
+            c.close();
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof AccountTypeWithDataSet)) return false;
+
+        AccountTypeWithDataSet other = (AccountTypeWithDataSet) o;
+        return Objects.equal(accountType, other.accountType)
+                && Objects.equal(dataSet, other.dataSet);
+    }
+
+    @Override
+    public int hashCode() {
+        return (accountType == null ? 0 : accountType.hashCode())
+                ^ (dataSet == null ? 0 : dataSet.hashCode());
+    }
+
+    @Override
+    public String toString() {
+        return "[" + accountType + "/" + dataSet + "]";
+    }
+}
diff --git a/src/com/android/contacts/common/model/account/AccountWithDataSet.java b/src/com/android/contacts/common/model/account/AccountWithDataSet.java
new file mode 100644
index 0000000..dd31dbc
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/AccountWithDataSet.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Wrapper for an account that includes a data set (which may be null).
+ */
+public class AccountWithDataSet extends Account {
+    private static final String STRINGIFY_SEPARATOR = "\u0001";
+    private static final String ARRAY_STRINGIFY_SEPARATOR = "\u0002";
+
+    private static final Pattern STRINGIFY_SEPARATOR_PAT =
+            Pattern.compile(Pattern.quote(STRINGIFY_SEPARATOR));
+    private static final Pattern ARRAY_STRINGIFY_SEPARATOR_PAT =
+            Pattern.compile(Pattern.quote(ARRAY_STRINGIFY_SEPARATOR));
+
+    public final String dataSet;
+    private final AccountTypeWithDataSet mAccountTypeWithDataSet;
+
+    private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID};
+    private static final Uri RAW_CONTACTS_URI_LIMIT_1 = RawContacts.CONTENT_URI.buildUpon()
+            .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1").build();
+
+
+    public AccountWithDataSet(String name, String type, String dataSet) {
+        super(name, type);
+        this.dataSet = dataSet;
+        mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
+    }
+
+    public AccountWithDataSet(Parcel in) {
+        super(in);
+        this.dataSet = in.readString();
+        mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeString(dataSet);
+    }
+
+    // For Parcelable
+    public static final Creator<AccountWithDataSet> CREATOR = new Creator<AccountWithDataSet>() {
+        public AccountWithDataSet createFromParcel(Parcel source) {
+            return new AccountWithDataSet(source);
+        }
+
+        public AccountWithDataSet[] newArray(int size) {
+            return new AccountWithDataSet[size];
+        }
+    };
+
+    public AccountTypeWithDataSet getAccountTypeWithDataSet() {
+        return mAccountTypeWithDataSet;
+    }
+
+    /**
+     * Return {@code true} if this account has any contacts in the database.
+     * Touches DB.  Don't use in the UI thread.
+     */
+    public boolean hasData(Context context) {
+        final String BASE_SELECTION =
+                RawContacts.ACCOUNT_TYPE + " = ?" + " AND " + RawContacts.ACCOUNT_NAME + " = ?";
+        final String selection;
+        final String[] args;
+        if (TextUtils.isEmpty(dataSet)) {
+            selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL";
+            args = new String[] {type, name};
+        } else {
+            selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?";
+            args = new String[] {type, name, dataSet};
+        }
+
+        final Cursor c = context.getContentResolver().query(RAW_CONTACTS_URI_LIMIT_1,
+                ID_PROJECTION, selection, args, null);
+        if (c == null) return false;
+        try {
+            return c.moveToFirst();
+        } finally {
+            c.close();
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        return (o instanceof AccountWithDataSet) && super.equals(o)
+                && Objects.equal(((AccountWithDataSet) o).dataSet, dataSet);
+    }
+
+    @Override
+    public int hashCode() {
+        return 31 * super.hashCode()
+                + (dataSet == null ? 0 : dataSet.hashCode());
+    }
+
+    @Override
+    public String toString() {
+        return "AccountWithDataSet {name=" + name + ", type=" + type + ", dataSet=" + dataSet + "}";
+    }
+
+    private static StringBuilder addStringified(StringBuilder sb, AccountWithDataSet account) {
+        sb.append(account.name);
+        sb.append(STRINGIFY_SEPARATOR);
+        sb.append(account.type);
+        sb.append(STRINGIFY_SEPARATOR);
+        if (!TextUtils.isEmpty(account.dataSet)) sb.append(account.dataSet);
+
+        return sb;
+    }
+
+    /**
+     * Pack the instance into a string.
+     */
+    public String stringify() {
+        return addStringified(new StringBuilder(), this).toString();
+    }
+
+    /**
+     * Unpack a string created by {@link #stringify}.
+     *
+     * @throws IllegalArgumentException if it's an invalid string.
+     */
+    public static AccountWithDataSet unstringify(String s) {
+        final String[] array = STRINGIFY_SEPARATOR_PAT.split(s, 3);
+        if (array.length < 3) {
+            throw new IllegalArgumentException("Invalid string " + s);
+        }
+        return new AccountWithDataSet(array[0], array[1],
+                TextUtils.isEmpty(array[2]) ? null : array[2]);
+    }
+
+    /**
+     * Pack a list of {@link AccountWithDataSet} into a string.
+     */
+    public static String stringifyList(List<AccountWithDataSet> accounts) {
+        final StringBuilder sb = new StringBuilder();
+
+        for (AccountWithDataSet account : accounts) {
+            if (sb.length() > 0) {
+                sb.append(ARRAY_STRINGIFY_SEPARATOR);
+            }
+            addStringified(sb, account);
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Unpack a list of {@link AccountWithDataSet} into a string.
+     *
+     * @throws IllegalArgumentException if it's an invalid string.
+     */
+    public static List<AccountWithDataSet> unstringifyList(String s) {
+        final ArrayList<AccountWithDataSet> ret = Lists.newArrayList();
+        if (TextUtils.isEmpty(s)) {
+            return ret;
+        }
+
+        final String[] array = ARRAY_STRINGIFY_SEPARATOR_PAT.split(s);
+
+        for (int i = 0; i < array.length; i++) {
+            ret.add(unstringify(array[i]));
+        }
+
+        return ret;
+    }
+}
diff --git a/src/com/android/contacts/common/model/account/BaseAccountType.java b/src/com/android/contacts/common/model/account/BaseAccountType.java
new file mode 100644
index 0000000..772657a
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/BaseAccountType.java
@@ -0,0 +1,1482 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+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.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.test.NeededForTesting;
+import com.android.contacts.common.util.CommonDateUtils;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public abstract class BaseAccountType extends AccountType {
+    private static final String TAG = "BaseAccountType";
+
+    protected static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE;
+    protected static final int FLAGS_EMAIL = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+    protected static final int FLAGS_PERSON_NAME = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
+    protected static final int FLAGS_PHONETIC = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_VARIATION_PHONETIC;
+    protected static final int FLAGS_GENERIC_NAME = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
+    protected static final int FLAGS_NOTE = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+    protected static final int FLAGS_EVENT = EditorInfo.TYPE_CLASS_TEXT;
+    protected static final int FLAGS_WEBSITE = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_VARIATION_URI;
+    protected static final int FLAGS_POSTAL = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
+            | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+    protected static final int FLAGS_SIP_ADDRESS = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;  // since SIP addresses have the same
+                                                             // basic format as email addresses
+    protected static final int FLAGS_RELATION = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
+
+    // Specify the maximum number of lines that can be used to display various field types.  If no
+    // value is specified for a particular type, we use the default value from {@link DataKind}.
+    protected static final int MAX_LINES_FOR_POSTAL_ADDRESS = 10;
+    protected static final int MAX_LINES_FOR_GROUP = 10;
+    protected static final int MAX_LINES_FOR_NOTE = 100;
+
+    private interface Tag {
+        static final String DATA_KIND = "DataKind";
+        static final String TYPE = "Type";
+    }
+
+    private interface Attr {
+        static final String MAX_OCCURRENCE = "maxOccurs";
+        static final String DATE_WITH_TIME = "dateWithTime";
+        static final String YEAR_OPTIONAL = "yearOptional";
+        static final String KIND = "kind";
+        static final String TYPE = "type";
+    }
+
+    private interface Weight {
+        static final int NONE = -1;
+        static final int ORGANIZATION = 5;
+        static final int PHONE = 10;
+        static final int EMAIL = 15;
+        static final int IM = 20;
+        static final int STRUCTURED_POSTAL = 25;
+        static final int NOTE = 110;
+        static final int NICKNAME = 115;
+        static final int WEBSITE = 120;
+        static final int SIP_ADDRESS = 130;
+        static final int EVENT = 150;
+        static final int RELATIONSHIP = 160;
+        static final int GROUP_MEMBERSHIP = 999;
+    }
+
+    public BaseAccountType() {
+        this.accountType = null;
+        this.dataSet = null;
+        this.titleRes = R.string.account_phone;
+        this.iconRes = R.mipmap.ic_launcher_contacts;
+    }
+
+    protected static EditType buildPhoneType(int type) {
+        return new EditType(type, Phone.getTypeLabelResource(type));
+    }
+
+    protected static EditType buildEmailType(int type) {
+        return new EditType(type, Email.getTypeLabelResource(type));
+    }
+
+    protected static EditType buildPostalType(int type) {
+        return new EditType(type, StructuredPostal.getTypeLabelResource(type));
+    }
+
+    protected static EditType buildImType(int type) {
+        return new EditType(type, Im.getProtocolLabelResource(type));
+    }
+
+    protected static EditType buildEventType(int type, boolean yearOptional) {
+        return new EventEditType(type, Event.getTypeResource(type)).setYearOptional(yearOptional);
+    }
+
+    protected static EditType buildRelationType(int type) {
+        return new EditType(type, Relation.getTypeLabelResource(type));
+    }
+
+    protected DataKind addDataKindStructuredName(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(StructuredName.CONTENT_ITEM_TYPE,
+                R.string.nameLabelsGroup, -1, true, R.layout.structured_name_editor_view));
+        kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+        kind.actionBody = new SimpleInflater(Nickname.NAME);
+        kind.typeOverallMax = 1;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(StructuredName.DISPLAY_NAME,
+                R.string.full_name, FLAGS_PERSON_NAME));
+        kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                FLAGS_PERSON_NAME).setLongForm(true));
+        kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                FLAGS_PERSON_NAME).setLongForm(true));
+        kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                FLAGS_PERSON_NAME).setLongForm(true));
+        kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                FLAGS_PERSON_NAME).setLongForm(true));
+        kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                FLAGS_PERSON_NAME).setLongForm(true));
+        kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                R.string.name_phonetic_family, FLAGS_PHONETIC));
+        kind.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
+                R.string.name_phonetic_middle, FLAGS_PHONETIC));
+        kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+        return kind;
+    }
+
+    protected DataKind addDataKindDisplayName(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
+                R.string.nameLabelsGroup, -1, true, R.layout.text_fields_editor_view));
+        kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+        kind.actionBody = new SimpleInflater(Nickname.NAME);
+        kind.typeOverallMax = 1;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(StructuredName.DISPLAY_NAME,
+                R.string.full_name, FLAGS_PERSON_NAME).setShortForm(true));
+
+        boolean displayOrderPrimary =
+                context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+
+        if (!displayOrderPrimary) {
+            kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+        } else {
+            kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+        }
+
+        return kind;
+    }
+
+    protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
+                R.string.name_phonetic, -1, true, R.layout.phonetic_name_editor_view));
+        kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+        kind.actionBody = new SimpleInflater(Nickname.NAME);
+        kind.typeOverallMax = 1;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME,
+                R.string.name_phonetic, FLAGS_PHONETIC).setShortForm(true));
+        kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                R.string.name_phonetic_family, FLAGS_PHONETIC).setLongForm(true));
+        kind.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
+                R.string.name_phonetic_middle, FLAGS_PHONETIC).setLongForm(true));
+        kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                R.string.name_phonetic_given, FLAGS_PHONETIC).setLongForm(true));
+
+        return kind;
+    }
+
+    protected DataKind addDataKindNickname(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(Nickname.CONTENT_ITEM_TYPE,
+                    R.string.nicknameLabelsGroup, 115, true, R.layout.text_fields_editor_view));
+        kind.typeOverallMax = 1;
+        kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup);
+        kind.actionBody = new SimpleInflater(Nickname.NAME);
+        kind.defaultValues = new ContentValues();
+        kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
+                FLAGS_PERSON_NAME));
+
+        return kind;
+    }
+
+    protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(Phone.CONTENT_ITEM_TYPE, R.string.phoneLabelsGroup,
+                10, true, R.layout.text_fields_editor_view));
+        kind.iconAltRes = R.drawable.ic_text_holo_light;
+        kind.iconAltDescriptionRes = R.string.sms;
+        kind.actionHeader = new PhoneActionInflater();
+        kind.actionAltHeader = new PhoneActionAltInflater();
+        kind.actionBody = new SimpleInflater(Phone.NUMBER);
+        kind.typeColumn = Phone.TYPE;
+        kind.typeList = Lists.newArrayList();
+        kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+        kind.typeList.add(
+                buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_CALLBACK).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_ISDN).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER_FAX).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_TELEX).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_TTY_TDD).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_MOBILE).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_PAGER).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true));
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+        return kind;
+    }
+
+    protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(Email.CONTENT_ITEM_TYPE, R.string.emailLabelsGroup,
+                15, true, R.layout.text_fields_editor_view));
+        kind.actionHeader = new EmailActionInflater();
+        kind.actionBody = new SimpleInflater(Email.DATA);
+        kind.typeColumn = Email.TYPE;
+        kind.typeList = Lists.newArrayList();
+        kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+        kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+        kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+        kind.typeList.add(buildEmailType(Email.TYPE_MOBILE));
+        kind.typeList.add(
+                buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL));
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+        return kind;
+    }
+
+    protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(StructuredPostal.CONTENT_ITEM_TYPE,
+                R.string.postalLabelsGroup, 25, true, R.layout.text_fields_editor_view));
+        kind.actionHeader = new PostalActionInflater();
+        kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
+        kind.typeColumn = StructuredPostal.TYPE;
+        kind.typeList = Lists.newArrayList();
+        kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME));
+        kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK));
+        kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER));
+        kind.typeList.add(buildPostalType(StructuredPostal.TYPE_CUSTOM).setSecondary(true)
+                .setCustomColumn(StructuredPostal.LABEL));
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(
+                new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address,
+                        FLAGS_POSTAL));
+
+        kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS;
+
+        return kind;
+    }
+
+    protected DataKind addDataKindIm(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup, 20, true,
+                    R.layout.text_fields_editor_view));
+        kind.actionHeader = new ImActionInflater();
+        kind.actionBody = new SimpleInflater(Im.DATA);
+
+        // NOTE: even though a traditional "type" exists, for editing
+        // purposes we're using the protocol to pick labels
+
+        kind.defaultValues = new ContentValues();
+        kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+        kind.typeColumn = Im.PROTOCOL;
+        kind.typeList = Lists.newArrayList();
+        kind.typeList.add(buildImType(Im.PROTOCOL_AIM));
+        kind.typeList.add(buildImType(Im.PROTOCOL_MSN));
+        kind.typeList.add(buildImType(Im.PROTOCOL_YAHOO));
+        kind.typeList.add(buildImType(Im.PROTOCOL_SKYPE));
+        kind.typeList.add(buildImType(Im.PROTOCOL_QQ));
+        kind.typeList.add(buildImType(Im.PROTOCOL_GOOGLE_TALK));
+        kind.typeList.add(buildImType(Im.PROTOCOL_ICQ));
+        kind.typeList.add(buildImType(Im.PROTOCOL_JABBER));
+        kind.typeList.add(buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true).setCustomColumn(
+                Im.CUSTOM_PROTOCOL));
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+        return kind;
+    }
+
+    protected DataKind addDataKindOrganization(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(Organization.CONTENT_ITEM_TYPE,
+                    R.string.organizationLabelsGroup, 5, true,
+                    R.layout.text_fields_editor_view));
+        kind.actionHeader = new SimpleInflater(Organization.COMPANY);
+        kind.actionBody = new SimpleInflater(Organization.TITLE);
+        kind.typeOverallMax = 1;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company,
+                FLAGS_GENERIC_NAME));
+        kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title,
+                FLAGS_GENERIC_NAME));
+
+        return kind;
+    }
+
+    protected DataKind addDataKindPhoto(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, true, -1));
+        kind.typeOverallMax = 1;
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+        return kind;
+    }
+
+    protected DataKind addDataKindNote(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(Note.CONTENT_ITEM_TYPE,
+                    R.string.label_notes, 110, true, R.layout.text_fields_editor_view));
+        kind.typeOverallMax = 1;
+        kind.actionHeader = new SimpleInflater(R.string.label_notes);
+        kind.actionBody = new SimpleInflater(Note.NOTE);
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+
+        kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;
+
+        return kind;
+    }
+
+    protected DataKind addDataKindWebsite(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(Website.CONTENT_ITEM_TYPE,
+                R.string.websiteLabelsGroup, 120, true, R.layout.text_fields_editor_view));
+        kind.actionHeader = new SimpleInflater(R.string.websiteLabelsGroup);
+        kind.actionBody = new SimpleInflater(Website.URL);
+        kind.defaultValues = new ContentValues();
+        kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+        return kind;
+    }
+
+    protected DataKind addDataKindSipAddress(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(SipAddress.CONTENT_ITEM_TYPE,
+                    R.string.label_sip_address, 130, true, R.layout.text_fields_editor_view));
+
+        kind.typeOverallMax = 1;
+        kind.actionHeader = new SimpleInflater(R.string.label_sip_address);
+        kind.actionBody = new SimpleInflater(SipAddress.SIP_ADDRESS);
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(SipAddress.SIP_ADDRESS,
+                                         R.string.label_sip_address, FLAGS_SIP_ADDRESS));
+
+        return kind;
+    }
+
+    protected DataKind addDataKindGroupMembership(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(GroupMembership.CONTENT_ITEM_TYPE,
+                R.string.groupsLabel, 999, true, -1));
+
+        kind.typeOverallMax = 1;
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
+
+        kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;
+
+        return kind;
+    }
+
+    /**
+     * Simple inflater that assumes a string resource has a "%s" that will be
+     * filled from the given column.
+     */
+    public static class SimpleInflater implements StringInflater {
+        private final int mStringRes;
+        private final String mColumnName;
+
+        public SimpleInflater(int stringRes) {
+            this(stringRes, null);
+        }
+
+        public SimpleInflater(String columnName) {
+            this(-1, columnName);
+        }
+
+        public SimpleInflater(int stringRes, String columnName) {
+            mStringRes = stringRes;
+            mColumnName = columnName;
+        }
+
+        @Override
+        public CharSequence inflateUsing(Context context, ContentValues values) {
+            final boolean validColumn = values.containsKey(mColumnName);
+            final boolean validString = mStringRes > 0;
+
+            final CharSequence stringValue = validString ? context.getText(mStringRes) : null;
+            final CharSequence columnValue = validColumn ? values.getAsString(mColumnName) : null;
+
+            if (validString && validColumn) {
+                return String.format(stringValue.toString(), columnValue);
+            } else if (validString) {
+                return stringValue;
+            } else if (validColumn) {
+                return columnValue;
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public String toString() {
+            return this.getClass().getSimpleName()
+                    + " mStringRes=" + mStringRes
+                    + " mColumnName" + mColumnName;
+        }
+
+        @NeededForTesting
+        public String getColumnNameForTest() {
+            return mColumnName;
+        }
+    }
+
+    public static abstract class CommonInflater implements StringInflater {
+        protected abstract int getTypeLabelResource(Integer type);
+
+        protected boolean isCustom(Integer type) {
+            return type == BaseTypes.TYPE_CUSTOM;
+        }
+
+        protected String getTypeColumn() {
+            return Phone.TYPE;
+        }
+
+        protected String getLabelColumn() {
+            return Phone.LABEL;
+        }
+
+        protected CharSequence getTypeLabel(Resources res, Integer type, CharSequence label) {
+            final int labelRes = getTypeLabelResource(type);
+            if (type == null) {
+                return res.getText(labelRes);
+            } else if (isCustom(type)) {
+                return res.getString(labelRes, label == null ? "" : label);
+            } else {
+                return res.getText(labelRes);
+            }
+        }
+
+        @Override
+        public CharSequence inflateUsing(Context context, ContentValues values) {
+            final Integer type = values.getAsInteger(getTypeColumn());
+            final String label = values.getAsString(getLabelColumn());
+            return getTypeLabel(context.getResources(), type, label);
+        }
+
+        @Override
+        public String toString() {
+            return this.getClass().getSimpleName();
+        }
+    }
+
+    public static class PhoneActionInflater extends CommonInflater {
+        @Override
+        protected boolean isCustom(Integer type) {
+            return ContactDisplayUtils.isCustomPhoneType(type);
+        }
+
+        @Override
+        protected int getTypeLabelResource(Integer type) {
+            return ContactDisplayUtils.getPhoneLabelResourceId(type);
+        }
+    }
+
+    public static class PhoneActionAltInflater extends CommonInflater {
+        @Override
+        protected boolean isCustom(Integer type) {
+            return ContactDisplayUtils.isCustomPhoneType(type);
+        }
+
+        @Override
+        protected int getTypeLabelResource(Integer type) {
+            return ContactDisplayUtils.getSmsLabelResourceId(type);
+        }
+    }
+
+    public static class EmailActionInflater extends CommonInflater {
+        @Override
+        protected int getTypeLabelResource(Integer type) {
+            if (type == null) return R.string.email;
+            switch (type) {
+                case Email.TYPE_HOME: return R.string.email_home;
+                case Email.TYPE_WORK: return R.string.email_work;
+                case Email.TYPE_OTHER: return R.string.email_other;
+                case Email.TYPE_MOBILE: return R.string.email_mobile;
+                default: return R.string.email_custom;
+            }
+        }
+    }
+
+    public static class EventActionInflater extends CommonInflater {
+        @Override
+        protected int getTypeLabelResource(Integer type) {
+            return Event.getTypeResource(type);
+        }
+    }
+
+    public static class RelationActionInflater extends CommonInflater {
+        @Override
+        protected int getTypeLabelResource(Integer type) {
+            return Relation.getTypeLabelResource(type == null ? Relation.TYPE_CUSTOM : type);
+        }
+    }
+
+    public static class PostalActionInflater extends CommonInflater {
+        @Override
+        protected int getTypeLabelResource(Integer type) {
+            if (type == null) return R.string.map_other;
+            switch (type) {
+                case StructuredPostal.TYPE_HOME: return R.string.map_home;
+                case StructuredPostal.TYPE_WORK: return R.string.map_work;
+                case StructuredPostal.TYPE_OTHER: return R.string.map_other;
+                default: return R.string.map_custom;
+            }
+        }
+    }
+
+    public static class ImActionInflater extends CommonInflater {
+        @Override
+        protected String getTypeColumn() {
+            return Im.PROTOCOL;
+        }
+
+        @Override
+        protected String getLabelColumn() {
+            return Im.CUSTOM_PROTOCOL;
+        }
+
+        @Override
+        protected int getTypeLabelResource(Integer type) {
+            if (type == null) return R.string.chat;
+            switch (type) {
+                case Im.PROTOCOL_AIM: return R.string.chat_aim;
+                case Im.PROTOCOL_MSN: return R.string.chat_msn;
+                case Im.PROTOCOL_YAHOO: return R.string.chat_yahoo;
+                case Im.PROTOCOL_SKYPE: return R.string.chat_skype;
+                case Im.PROTOCOL_QQ: return R.string.chat_qq;
+                case Im.PROTOCOL_GOOGLE_TALK: return R.string.chat_gtalk;
+                case Im.PROTOCOL_ICQ: return R.string.chat_icq;
+                case Im.PROTOCOL_JABBER: return R.string.chat_jabber;
+                case Im.PROTOCOL_NETMEETING: return R.string.chat;
+                default: return R.string.chat;
+            }
+        }
+    }
+
+    @Override
+    public boolean isGroupMembershipEditable() {
+        return false;
+    }
+
+    /**
+     * Parses the content of the EditSchema tag in contacts.xml.
+     */
+    protected final void parseEditSchema(Context context, XmlPullParser parser, AttributeSet attrs)
+            throws XmlPullParserException, IOException, DefinitionException {
+
+        final int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            final int depth = parser.getDepth();
+            if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
+                continue; // Not direct child tag
+            }
+
+            final String tag = parser.getName();
+
+            if (Tag.DATA_KIND.equals(tag)) {
+                for (DataKind kind : KindParser.INSTANCE.parseDataKindTag(context, parser, attrs)) {
+                    addKind(kind);
+                }
+            } else {
+                Log.w(TAG, "Skipping unknown tag " + tag);
+            }
+        }
+    }
+
+    // Utility methods to keep code shorter.
+    private static boolean getAttr(AttributeSet attrs, String attribute, boolean defaultValue) {
+        return attrs.getAttributeBooleanValue(null, attribute, defaultValue);
+    }
+
+    private static int getAttr(AttributeSet attrs, String attribute, int defaultValue) {
+        return attrs.getAttributeIntValue(null, attribute, defaultValue);
+    }
+
+    private static String getAttr(AttributeSet attrs, String attribute) {
+        return attrs.getAttributeValue(null, attribute);
+    }
+
+    // TODO Extract it to its own class, and move all KindBuilders to it as well.
+    private static class KindParser {
+        public static final KindParser INSTANCE = new KindParser();
+
+        private final Map<String, KindBuilder> mBuilders = Maps.newHashMap();
+
+        private KindParser() {
+            addBuilder(new NameKindBuilder());
+            addBuilder(new NicknameKindBuilder());
+            addBuilder(new PhoneKindBuilder());
+            addBuilder(new EmailKindBuilder());
+            addBuilder(new StructuredPostalKindBuilder());
+            addBuilder(new ImKindBuilder());
+            addBuilder(new OrganizationKindBuilder());
+            addBuilder(new PhotoKindBuilder());
+            addBuilder(new NoteKindBuilder());
+            addBuilder(new WebsiteKindBuilder());
+            addBuilder(new SipAddressKindBuilder());
+            addBuilder(new GroupMembershipKindBuilder());
+            addBuilder(new EventKindBuilder());
+            addBuilder(new RelationshipKindBuilder());
+        }
+
+        private void addBuilder(KindBuilder builder) {
+            mBuilders.put(builder.getTagName(), builder);
+        }
+
+        /**
+         * Takes a {@link XmlPullParser} at the start of a DataKind tag, parses it and returns
+         * {@link DataKind}s.  (Usually just one, but there are three for the "name" kind.)
+         *
+         * This method returns a list, because we need to add 3 kinds for the name data kind.
+         * (structured, display and phonetic)
+         */
+        public List<DataKind> parseDataKindTag(Context context, XmlPullParser parser,
+                AttributeSet attrs)
+                throws DefinitionException, XmlPullParserException, IOException {
+            final String kind = getAttr(attrs, Attr.KIND);
+            final KindBuilder builder = mBuilders.get(kind);
+            if (builder != null) {
+                return builder.parseDataKind(context, parser, attrs);
+            } else {
+                throw new DefinitionException("Undefined data kind '" + kind + "'");
+            }
+        }
+    }
+
+    private static abstract class KindBuilder {
+
+        public abstract String getTagName();
+
+        /**
+         * DataKind tag parser specific to each kind.  Subclasses must implement it.
+         */
+        public abstract List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException, IOException;
+
+        /**
+         * Creates a new {@link DataKind}, and also parses the child Type tags in the DataKind
+         * tag.
+         */
+        protected final DataKind newDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs, boolean isPseudo, String mimeType, String typeColumn,
+                int titleRes, int weight, int editorLayoutResourceId,
+                StringInflater actionHeader, StringInflater actionBody)
+                throws DefinitionException, XmlPullParserException, IOException {
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Adding DataKind: " + mimeType);
+            }
+
+            final DataKind kind = new DataKind(mimeType, titleRes, weight, true,
+                    editorLayoutResourceId);
+            kind.typeColumn = typeColumn;
+            kind.actionHeader = actionHeader;
+            kind.actionBody = actionBody;
+            kind.fieldList = Lists.newArrayList();
+
+            // Get more information from the tag...
+            // A pseudo data kind doesn't have corresponding tag the XML, so we skip this.
+            if (!isPseudo) {
+                kind.typeOverallMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
+
+                // Process "Type" tags.
+                // If a kind has the type column, contacts.xml must have at least one type
+                // definition.  Otherwise, it mustn't have a type definition.
+                if (kind.typeColumn != null) {
+                    // Parse and add types.
+                    kind.typeList = Lists.newArrayList();
+                    parseTypes(context, parser, attrs, kind, true);
+                    if (kind.typeList.size() == 0) {
+                        throw new DefinitionException(
+                                "Kind " + kind.mimeType + " must have at least one type");
+                    }
+                } else {
+                    // Make sure it has no types.
+                    parseTypes(context, parser, attrs, kind, false /* can't have types */);
+                }
+            }
+
+            return kind;
+        }
+
+        /**
+         * Parses Type elements in a DataKind element, and if {@code canHaveTypes} is true adds
+         * them to the given {@link DataKind}. Otherwise the {@link DataKind} can't have a type,
+         * so throws {@link DefinitionException}.
+         */
+        private void parseTypes(Context context, XmlPullParser parser, AttributeSet attrs,
+                DataKind kind, boolean canHaveTypes)
+                throws DefinitionException, XmlPullParserException, IOException {
+            final int outerDepth = parser.getDepth();
+            int type;
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+                final int depth = parser.getDepth();
+                if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
+                    continue; // Not direct child tag
+                }
+
+                final String tag = parser.getName();
+                if (Tag.TYPE.equals(tag)) {
+                    if (canHaveTypes) {
+                        kind.typeList.add(parseTypeTag(parser, attrs, kind));
+                    } else {
+                        throw new DefinitionException(
+                                "Kind " + kind.mimeType + " can't have types");
+                    }
+                } else {
+                    throw new DefinitionException("Unknown tag: " + tag);
+                }
+            }
+        }
+
+        /**
+         * Parses a single Type element and returns an {@link EditType} built from it.  Uses
+         * {@link #buildEditTypeForTypeTag} defined in subclasses to actually build an
+         * {@link EditType}.
+         */
+        private EditType parseTypeTag(XmlPullParser parser, AttributeSet attrs, DataKind kind)
+                throws DefinitionException {
+
+            final String typeName = getAttr(attrs, Attr.TYPE);
+
+            final EditType et = buildEditTypeForTypeTag(attrs, typeName);
+            if (et == null) {
+                throw new DefinitionException(
+                        "Undefined type '" + typeName + "' for data kind '" + kind.mimeType + "'");
+            }
+            et.specificMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
+
+            return et;
+        }
+
+        /**
+         * Returns an {@link EditType} for the given "type".  Subclasses may optionally use
+         * the attributes in the tag to set optional values.
+         * (e.g. "yearOptional" for the event kind)
+         */
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            return null;
+        }
+
+        protected final void throwIfList(DataKind kind) throws DefinitionException {
+            if (kind.typeOverallMax != 1) {
+                throw new DefinitionException(
+                        "Kind " + kind.mimeType + " must have 'overallMax=\"1\"'");
+            }
+        }
+    }
+
+    /**
+     * DataKind parser for Name. (structured, display, phonetic)
+     */
+    private static class NameKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "name";
+        }
+
+        private static void checkAttributeTrue(boolean value, String attrName)
+                throws DefinitionException {
+            if (!value) {
+                throw new DefinitionException(attrName + " must be true");
+            }
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+
+            // Build 3 data kinds:
+            // - StructuredName.CONTENT_ITEM_TYPE
+            // - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME
+            // - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME
+
+            final boolean displayOrderPrimary =
+                    context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+
+            final boolean supportsDisplayName = getAttr(attrs, "supportsDisplayName", false);
+            final boolean supportsPrefix = getAttr(attrs, "supportsPrefix", false);
+            final boolean supportsMiddleName = getAttr(attrs, "supportsMiddleName", false);
+            final boolean supportsSuffix = getAttr(attrs, "supportsSuffix", false);
+            final boolean supportsPhoneticFamilyName =
+                    getAttr(attrs, "supportsPhoneticFamilyName", false);
+            final boolean supportsPhoneticMiddleName =
+                    getAttr(attrs, "supportsPhoneticMiddleName", false);
+            final boolean supportsPhoneticGivenName =
+                    getAttr(attrs, "supportsPhoneticGivenName", false);
+
+            // For now, every things must be supported.
+            checkAttributeTrue(supportsDisplayName, "supportsDisplayName");
+            checkAttributeTrue(supportsPrefix, "supportsPrefix");
+            checkAttributeTrue(supportsMiddleName, "supportsMiddleName");
+            checkAttributeTrue(supportsSuffix, "supportsSuffix");
+            checkAttributeTrue(supportsPhoneticFamilyName, "supportsPhoneticFamilyName");
+            checkAttributeTrue(supportsPhoneticMiddleName, "supportsPhoneticMiddleName");
+            checkAttributeTrue(supportsPhoneticGivenName, "supportsPhoneticGivenName");
+
+            final List<DataKind> kinds = Lists.newArrayList();
+
+            // Structured name
+            final DataKind ks = newDataKind(context, parser, attrs, false,
+                    StructuredName.CONTENT_ITEM_TYPE, null, R.string.nameLabelsGroup, Weight.NONE,
+                    R.layout.structured_name_editor_view,
+                    new SimpleInflater(R.string.nameLabelsGroup),
+                    new SimpleInflater(Nickname.NAME));
+
+            throwIfList(ks);
+            kinds.add(ks);
+
+            // Note about setLongForm/setShortForm below.
+            // We need to set this only when the type supports display name. (=supportsDisplayName)
+            // Otherwise (i.e. Exchange) we don't set these flags, but instead make some fields
+            // "optional".
+
+            ks.fieldList.add(new EditField(StructuredName.DISPLAY_NAME, R.string.full_name,
+                    FLAGS_PERSON_NAME));
+            ks.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            ks.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            ks.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            ks.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            ks.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            ks.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                    R.string.name_phonetic_family, FLAGS_PHONETIC));
+            ks.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
+                    R.string.name_phonetic_middle, FLAGS_PHONETIC));
+            ks.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                    R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+            // Display name
+            final DataKind kd = newDataKind(context, parser, attrs, true,
+                    DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, null,
+                    R.string.nameLabelsGroup, Weight.NONE, R.layout.text_fields_editor_view,
+                    new SimpleInflater(R.string.nameLabelsGroup),
+                    new SimpleInflater(Nickname.NAME));
+            kd.typeOverallMax = 1;
+            kinds.add(kd);
+
+            kd.fieldList.add(new EditField(StructuredName.DISPLAY_NAME,
+                    R.string.full_name, FLAGS_PERSON_NAME).setShortForm(true));
+
+            if (!displayOrderPrimary) {
+                kd.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+            } else {
+                kd.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+            }
+
+            // Phonetic name
+            final DataKind kp = newDataKind(context, parser, attrs, true,
+                    DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, null,
+                    R.string.name_phonetic, Weight.NONE, R.layout.phonetic_name_editor_view,
+                    new SimpleInflater(R.string.nameLabelsGroup),
+                    new SimpleInflater(Nickname.NAME));
+            kp.typeOverallMax = 1;
+            kinds.add(kp);
+
+            // We may want to change the order depending on displayOrderPrimary too.
+            kp.fieldList.add(new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME,
+                    R.string.name_phonetic, FLAGS_PHONETIC).setShortForm(true));
+            kp.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                    R.string.name_phonetic_family, FLAGS_PHONETIC).setLongForm(true));
+            kp.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
+                    R.string.name_phonetic_middle, FLAGS_PHONETIC).setLongForm(true));
+            kp.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                    R.string.name_phonetic_given, FLAGS_PHONETIC).setLongForm(true));
+            return kinds;
+        }
+    }
+
+    private static class NicknameKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "nickname";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Nickname.CONTENT_ITEM_TYPE, null, R.string.nicknameLabelsGroup, Weight.NICKNAME,
+                    R.layout.text_fields_editor_view,
+                    new SimpleInflater(R.string.nicknameLabelsGroup),
+                    new SimpleInflater(Nickname.NAME));
+
+            kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
+                    FLAGS_PERSON_NAME));
+
+            kind.defaultValues = new ContentValues();
+            kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+
+            throwIfList(kind);
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    private static class PhoneKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "phone";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Phone.CONTENT_ITEM_TYPE, Phone.TYPE, R.string.phoneLabelsGroup, Weight.PHONE,
+                    R.layout.text_fields_editor_view,
+                    new PhoneActionInflater(), new SimpleInflater(Phone.NUMBER));
+
+            kind.iconAltRes = R.drawable.ic_text_holo_light;
+            kind.iconAltDescriptionRes = R.string.sms;
+            kind.actionAltHeader = new PhoneActionAltInflater();
+
+            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+            return Lists.newArrayList(kind);
+        }
+
+        /** Just to avoid line-wrapping... */
+        protected static EditType build(int type, boolean secondary) {
+            return new EditType(type, Phone.getTypeLabelResource(type)).setSecondary(secondary);
+        }
+
+        @Override
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            if ("home".equals(type)) return build(Phone.TYPE_HOME, false);
+            if ("mobile".equals(type)) return build(Phone.TYPE_MOBILE, false);
+            if ("work".equals(type)) return build(Phone.TYPE_WORK, false);
+            if ("fax_work".equals(type)) return build(Phone.TYPE_FAX_WORK, true);
+            if ("fax_home".equals(type)) return build(Phone.TYPE_FAX_HOME, true);
+            if ("pager".equals(type)) return build(Phone.TYPE_PAGER, true);
+            if ("other".equals(type)) return build(Phone.TYPE_OTHER, false);
+            if ("callback".equals(type)) return build(Phone.TYPE_CALLBACK, true);
+            if ("car".equals(type)) return build(Phone.TYPE_CAR, true);
+            if ("company_main".equals(type)) return build(Phone.TYPE_COMPANY_MAIN, true);
+            if ("isdn".equals(type)) return build(Phone.TYPE_ISDN, true);
+            if ("main".equals(type)) return build(Phone.TYPE_MAIN, true);
+            if ("other_fax".equals(type)) return build(Phone.TYPE_OTHER_FAX, true);
+            if ("radio".equals(type)) return build(Phone.TYPE_RADIO, true);
+            if ("telex".equals(type)) return build(Phone.TYPE_TELEX, true);
+            if ("tty_tdd".equals(type)) return build(Phone.TYPE_TTY_TDD, true);
+            if ("work_mobile".equals(type)) return build(Phone.TYPE_WORK_MOBILE, true);
+            if ("work_pager".equals(type)) return build(Phone.TYPE_WORK_PAGER, true);
+
+            // Note "assistant" used to be a custom column for the fallback type, but not anymore.
+            if ("assistant".equals(type)) return build(Phone.TYPE_ASSISTANT, true);
+            if ("mms".equals(type)) return build(Phone.TYPE_MMS, true);
+            if ("custom".equals(type)) {
+                return build(Phone.TYPE_CUSTOM, true).setCustomColumn(Phone.LABEL);
+            }
+            return null;
+        }
+    }
+
+    private static class EmailKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "email";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Email.CONTENT_ITEM_TYPE, Email.TYPE, R.string.emailLabelsGroup, Weight.EMAIL,
+                    R.layout.text_fields_editor_view,
+                    new EmailActionInflater(), new SimpleInflater(Email.DATA));
+            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+            return Lists.newArrayList(kind);
+        }
+
+        @Override
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            // EditType is mutable, so we need to create a new instance every time.
+            if ("home".equals(type)) return buildEmailType(Email.TYPE_HOME);
+            if ("work".equals(type)) return buildEmailType(Email.TYPE_WORK);
+            if ("other".equals(type)) return buildEmailType(Email.TYPE_OTHER);
+            if ("mobile".equals(type)) return buildEmailType(Email.TYPE_MOBILE);
+            if ("custom".equals(type)) {
+                return buildEmailType(Email.TYPE_CUSTOM)
+                        .setSecondary(true).setCustomColumn(Email.LABEL);
+            }
+            return null;
+        }
+    }
+
+    private static class StructuredPostalKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "postal";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE,
+                    R.string.postalLabelsGroup, Weight.STRUCTURED_POSTAL,
+                    R.layout.text_fields_editor_view, new PostalActionInflater(),
+                    new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS));
+
+            if (getAttr(attrs, "needsStructured", false)) {
+                if (Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage())) {
+                    // Japanese order
+                    kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
+                            R.string.postal_country, FLAGS_POSTAL).setOptional(true));
+                    kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
+                            R.string.postal_postcode, FLAGS_POSTAL));
+                    kind.fieldList.add(new EditField(StructuredPostal.REGION,
+                            R.string.postal_region, FLAGS_POSTAL));
+                    kind.fieldList.add(new EditField(StructuredPostal.CITY,
+                            R.string.postal_city,FLAGS_POSTAL));
+                    kind.fieldList.add(new EditField(StructuredPostal.STREET,
+                            R.string.postal_street, FLAGS_POSTAL));
+                } else {
+                    // Generic order
+                    kind.fieldList.add(new EditField(StructuredPostal.STREET,
+                            R.string.postal_street, FLAGS_POSTAL));
+                    kind.fieldList.add(new EditField(StructuredPostal.CITY,
+                            R.string.postal_city,FLAGS_POSTAL));
+                    kind.fieldList.add(new EditField(StructuredPostal.REGION,
+                            R.string.postal_region, FLAGS_POSTAL));
+                    kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
+                            R.string.postal_postcode, FLAGS_POSTAL));
+                    kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
+                            R.string.postal_country, FLAGS_POSTAL).setOptional(true));
+                }
+            } else {
+                kind.maxLinesForDisplay= MAX_LINES_FOR_POSTAL_ADDRESS;
+                kind.fieldList.add(
+                        new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address,
+                                FLAGS_POSTAL));
+            }
+
+            return Lists.newArrayList(kind);
+        }
+
+        @Override
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            // EditType is mutable, so we need to create a new instance every time.
+            if ("home".equals(type)) return buildPostalType(StructuredPostal.TYPE_HOME);
+            if ("work".equals(type)) return buildPostalType(StructuredPostal.TYPE_WORK);
+            if ("other".equals(type)) return buildPostalType(StructuredPostal.TYPE_OTHER);
+            if ("custom".equals(type)) {
+                return buildPostalType(StructuredPostal.TYPE_CUSTOM)
+                        .setSecondary(true).setCustomColumn(Email.LABEL);
+            }
+            return null;
+        }
+    }
+
+    private static class ImKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "im";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+
+            // IM is special:
+            // - It uses "protocol" as the custom label field
+            // - Its TYPE is fixed to TYPE_OTHER
+
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Im.CONTENT_ITEM_TYPE, Im.PROTOCOL, R.string.imLabelsGroup, Weight.IM,
+                    R.layout.text_fields_editor_view,
+                    new ImActionInflater(), new SimpleInflater(Im.DATA) // header / action
+                    );
+            kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+            kind.defaultValues = new ContentValues();
+            kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+            return Lists.newArrayList(kind);
+        }
+
+        @Override
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            if ("aim".equals(type)) return buildImType(Im.PROTOCOL_AIM);
+            if ("msn".equals(type)) return buildImType(Im.PROTOCOL_MSN);
+            if ("yahoo".equals(type)) return buildImType(Im.PROTOCOL_YAHOO);
+            if ("skype".equals(type)) return buildImType(Im.PROTOCOL_SKYPE);
+            if ("qq".equals(type)) return buildImType(Im.PROTOCOL_QQ);
+            if ("google_talk".equals(type)) return buildImType(Im.PROTOCOL_GOOGLE_TALK);
+            if ("icq".equals(type)) return buildImType(Im.PROTOCOL_ICQ);
+            if ("jabber".equals(type)) return buildImType(Im.PROTOCOL_JABBER);
+            if ("custom".equals(type)) {
+                return buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true)
+                        .setCustomColumn(Im.CUSTOM_PROTOCOL);
+            }
+            return null;
+        }
+    }
+
+    private static class OrganizationKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "organization";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Organization.CONTENT_ITEM_TYPE, null, R.string.organizationLabelsGroup,
+                    Weight.ORGANIZATION, R.layout.text_fields_editor_view ,
+                    new SimpleInflater(Organization.COMPANY),
+                    new SimpleInflater(Organization.TITLE));
+
+            kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company,
+                    FLAGS_GENERIC_NAME));
+            kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title,
+                    FLAGS_GENERIC_NAME));
+
+            throwIfList(kind);
+
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    private static class PhotoKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "photo";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Photo.CONTENT_ITEM_TYPE, null /* no type */, -1, Weight.NONE, -1,
+                    null, null // no header, no body
+                    );
+
+            kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
+            throwIfList(kind);
+
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    private static class NoteKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "note";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Note.CONTENT_ITEM_TYPE, null, R.string.label_notes, Weight.NOTE,
+                    R.layout.text_fields_editor_view,
+                    new SimpleInflater(R.string.label_notes), new SimpleInflater(Note.NOTE));
+
+            kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+            kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;
+
+            throwIfList(kind);
+
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    private static class WebsiteKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "website";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Website.CONTENT_ITEM_TYPE, null, R.string.websiteLabelsGroup, Weight.WEBSITE,
+                    R.layout.text_fields_editor_view,
+                    new SimpleInflater(R.string.websiteLabelsGroup),
+                    new SimpleInflater(Website.URL));
+
+            kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup,
+                    FLAGS_WEBSITE));
+
+            kind.defaultValues = new ContentValues();
+            kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
+
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    private static class SipAddressKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "sip_address";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    SipAddress.CONTENT_ITEM_TYPE, null, R.string.label_sip_address,
+                    Weight.SIP_ADDRESS, R.layout.text_fields_editor_view,
+                    new SimpleInflater(R.string.label_sip_address),
+                    new SimpleInflater(SipAddress.SIP_ADDRESS));
+
+            kind.fieldList.add(new EditField(SipAddress.SIP_ADDRESS,
+                    R.string.label_sip_address, FLAGS_SIP_ADDRESS));
+
+            throwIfList(kind);
+
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    private static class GroupMembershipKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "group_membership";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    GroupMembership.CONTENT_ITEM_TYPE, null,
+                    R.string.groupsLabel, Weight.GROUP_MEMBERSHIP, -1, null, null);
+
+            kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
+            kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;
+
+            throwIfList(kind);
+
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    /**
+     * Event DataKind parser.
+     *
+     * Event DataKind is used only for Google/Exchange types, so this parser is not used for now.
+     */
+    private static class EventKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "event";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Event.CONTENT_ITEM_TYPE, Event.TYPE, R.string.eventLabelsGroup, Weight.EVENT,
+                    R.layout.event_field_editor_view,
+                    new EventActionInflater(), new SimpleInflater(Event.START_DATE));
+
+            kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+            if (getAttr(attrs, Attr.DATE_WITH_TIME, false)) {
+                kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_AND_TIME_FORMAT;
+                kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT;
+            } else {
+                kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
+                kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
+            }
+
+            return Lists.newArrayList(kind);
+        }
+
+        @Override
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            final boolean yo = getAttr(attrs, Attr.YEAR_OPTIONAL, false);
+
+            if ("birthday".equals(type)) {
+                return buildEventType(Event.TYPE_BIRTHDAY, yo).setSpecificMax(1);
+            }
+            if ("anniversary".equals(type)) return buildEventType(Event.TYPE_ANNIVERSARY, yo);
+            if ("other".equals(type)) return buildEventType(Event.TYPE_OTHER, yo);
+            if ("custom".equals(type)) {
+                return buildEventType(Event.TYPE_CUSTOM, yo)
+                        .setSecondary(true).setCustomColumn(Event.LABEL);
+            }
+            return null;
+        }
+    }
+
+    /**
+     * Relationship DataKind parser.
+     *
+     * Relationship DataKind is used only for Google/Exchange types, so this parser is not used for
+     * now.
+     */
+    private static class RelationshipKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "relationship";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Relation.CONTENT_ITEM_TYPE, Relation.TYPE,
+                    R.string.relationLabelsGroup, Weight.RELATIONSHIP,
+                    R.layout.text_fields_editor_view,
+                    new RelationActionInflater(), new SimpleInflater(Relation.NAME));
+
+            kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup,
+                    FLAGS_RELATION));
+
+            kind.defaultValues = new ContentValues();
+            kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+            return Lists.newArrayList(kind);
+        }
+
+        @Override
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            // EditType is mutable, so we need to create a new instance every time.
+            if ("assistant".equals(type)) return buildRelationType(Relation.TYPE_ASSISTANT);
+            if ("brother".equals(type)) return buildRelationType(Relation.TYPE_BROTHER);
+            if ("child".equals(type)) return buildRelationType(Relation.TYPE_CHILD);
+            if ("domestic_partner".equals(type)) {
+                    return buildRelationType(Relation.TYPE_DOMESTIC_PARTNER);
+            }
+            if ("father".equals(type)) return buildRelationType(Relation.TYPE_FATHER);
+            if ("friend".equals(type)) return buildRelationType(Relation.TYPE_FRIEND);
+            if ("manager".equals(type)) return buildRelationType(Relation.TYPE_MANAGER);
+            if ("mother".equals(type)) return buildRelationType(Relation.TYPE_MOTHER);
+            if ("parent".equals(type)) return buildRelationType(Relation.TYPE_PARENT);
+            if ("partner".equals(type)) return buildRelationType(Relation.TYPE_PARTNER);
+            if ("referred_by".equals(type)) return buildRelationType(Relation.TYPE_REFERRED_BY);
+            if ("relative".equals(type)) return buildRelationType(Relation.TYPE_RELATIVE);
+            if ("sister".equals(type)) return buildRelationType(Relation.TYPE_SISTER);
+            if ("spouse".equals(type)) return buildRelationType(Relation.TYPE_SPOUSE);
+            if ("custom".equals(type)) {
+                return buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true)
+                        .setCustomColumn(Relation.LABEL);
+            }
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/model/account/ExchangeAccountType.java b/src/com/android/contacts/common/model/account/ExchangeAccountType.java
new file mode 100644
index 0000000..300e4d4
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/ExchangeAccountType.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+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 com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import com.google.common.collect.Lists;
+
+import java.util.Locale;
+
+public class ExchangeAccountType extends BaseAccountType {
+    private static final String TAG = "ExchangeAccountType";
+
+    public static final String ACCOUNT_TYPE_AOSP = "com.android.exchange";
+    public static final String ACCOUNT_TYPE_GOOGLE = "com.google.android.exchange";
+
+    public ExchangeAccountType(Context context, String authenticatorPackageName, String type) {
+        this.accountType = type;
+        this.resourcePackageName = null;
+        this.syncAdapterPackageName = authenticatorPackageName;
+
+        try {
+            addDataKindStructuredName(context);
+            addDataKindDisplayName(context);
+            addDataKindPhoneticName(context);
+            addDataKindNickname(context);
+            addDataKindPhone(context);
+            addDataKindEmail(context);
+            addDataKindStructuredPostal(context);
+            addDataKindIm(context);
+            addDataKindOrganization(context);
+            addDataKindPhoto(context);
+            addDataKindNote(context);
+            addDataKindEvent(context);
+            addDataKindWebsite(context);
+            addDataKindGroupMembership(context);
+
+            mIsInitialized = true;
+        } catch (DefinitionException e) {
+            Log.e(TAG, "Problem building account type", e);
+        }
+    }
+
+    public static boolean isExchangeType(String type) {
+        return ACCOUNT_TYPE_AOSP.equals(type) || ACCOUNT_TYPE_GOOGLE.equals(type);
+    }
+
+    @Override
+    protected DataKind addDataKindStructuredName(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(StructuredName.CONTENT_ITEM_TYPE,
+                R.string.nameLabelsGroup, -1, true, R.layout.structured_name_editor_view));
+        kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+        kind.actionBody = new SimpleInflater(Nickname.NAME);
+
+        kind.typeOverallMax = 1;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                FLAGS_PERSON_NAME).setOptional(true));
+        kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME,
+                R.string.name_family, FLAGS_PERSON_NAME));
+        kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME,
+                R.string.name_middle, FLAGS_PERSON_NAME));
+        kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME,
+                R.string.name_given, FLAGS_PERSON_NAME));
+        kind.fieldList.add(new EditField(StructuredName.SUFFIX,
+                R.string.name_suffix, FLAGS_PERSON_NAME));
+
+        kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                R.string.name_phonetic_family, FLAGS_PHONETIC));
+        kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind addDataKindDisplayName(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
+                R.string.nameLabelsGroup, -1, true, R.layout.text_fields_editor_view));
+
+        boolean displayOrderPrimary =
+                context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+        kind.typeOverallMax = 1;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                FLAGS_PERSON_NAME).setOptional(true));
+        if (!displayOrderPrimary) {
+            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME,
+                    R.string.name_family, FLAGS_PERSON_NAME));
+            kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME,
+                    R.string.name_middle, FLAGS_PERSON_NAME).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME,
+                    R.string.name_given, FLAGS_PERSON_NAME));
+        } else {
+            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME,
+                    R.string.name_given, FLAGS_PERSON_NAME));
+            kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME,
+                    R.string.name_middle, FLAGS_PERSON_NAME).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME,
+                    R.string.name_family, FLAGS_PERSON_NAME));
+        }
+        kind.fieldList.add(new EditField(StructuredName.SUFFIX,
+                R.string.name_suffix, FLAGS_PERSON_NAME).setOptional(true));
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
+                R.string.name_phonetic, -1, true, R.layout.phonetic_name_editor_view));
+        kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+        kind.actionBody = new SimpleInflater(Nickname.NAME);
+
+        kind.typeOverallMax = 1;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                R.string.name_phonetic_family, FLAGS_PHONETIC));
+        kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind addDataKindNickname(Context context) throws DefinitionException {
+        final DataKind kind = super.addDataKindNickname(context);
+
+        kind.typeOverallMax = 1;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
+                FLAGS_PERSON_NAME));
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+        final DataKind kind = super.addDataKindPhone(context);
+
+        kind.typeColumn = Phone.TYPE;
+        kind.typeList = Lists.newArrayList();
+        kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE).setSpecificMax(1));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_HOME).setSpecificMax(2));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_WORK).setSpecificMax(2));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true)
+                .setSpecificMax(1));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true)
+                .setSpecificMax(1));
+        kind.typeList
+                .add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true).setSpecificMax(1));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true).setSpecificMax(1));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true)
+                .setSpecificMax(1));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true).setSpecificMax(1));
+        kind.typeList
+                .add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true).setSpecificMax(1));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true)
+                .setSpecificMax(1));
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+        final DataKind kind = super.addDataKindEmail(context);
+
+        kind.typeOverallMax = 3;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
+        final DataKind kind = super.addDataKindStructuredPostal(context);
+
+        final boolean useJapaneseOrder =
+            Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
+        kind.typeColumn = StructuredPostal.TYPE;
+        kind.typeList = Lists.newArrayList();
+        kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1));
+        kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1));
+        kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1));
+
+        kind.fieldList = Lists.newArrayList();
+        if (useJapaneseOrder) {
+            kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
+                    R.string.postal_country, FLAGS_POSTAL).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
+                    R.string.postal_postcode, FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.REGION,
+                    R.string.postal_region, FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.CITY,
+                    R.string.postal_city,FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.STREET,
+                    R.string.postal_street, FLAGS_POSTAL));
+        } else {
+            kind.fieldList.add(new EditField(StructuredPostal.STREET,
+                    R.string.postal_street, FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.CITY,
+                    R.string.postal_city,FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.REGION,
+                    R.string.postal_region, FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
+                    R.string.postal_postcode, FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
+                    R.string.postal_country, FLAGS_POSTAL).setOptional(true));
+        }
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind addDataKindIm(Context context) throws DefinitionException {
+        final DataKind kind = super.addDataKindIm(context);
+
+        // Types are not supported for IM. There can be 3 IMs, but OWA only shows only the first
+        kind.typeOverallMax = 3;
+
+        kind.defaultValues = new ContentValues();
+        kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind addDataKindOrganization(Context context) throws DefinitionException {
+        final DataKind kind = super.addDataKindOrganization(context);
+
+        kind.typeOverallMax = 1;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company,
+                FLAGS_GENERIC_NAME));
+        kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title,
+                FLAGS_GENERIC_NAME));
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind addDataKindPhoto(Context context) throws DefinitionException {
+        final DataKind kind = super.addDataKindPhoto(context);
+
+        kind.typeOverallMax = 1;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind addDataKindNote(Context context) throws DefinitionException {
+        final DataKind kind = super.addDataKindNote(context);
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+
+        return kind;
+    }
+
+    protected DataKind addDataKindEvent(Context context) throws DefinitionException {
+        DataKind kind = addKind(
+                new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, 150, true,
+                R.layout.event_field_editor_view));
+        kind.actionHeader = new EventActionInflater();
+        kind.actionBody = new SimpleInflater(Event.START_DATE);
+
+        kind.typeOverallMax = 1;
+
+        kind.typeColumn = Event.TYPE;
+        kind.typeList = Lists.newArrayList();
+        kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, false).setSpecificMax(1));
+
+        kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind addDataKindWebsite(Context context) throws DefinitionException {
+        final DataKind kind = super.addDataKindWebsite(context);
+
+        kind.typeOverallMax = 1;
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+        return kind;
+    }
+
+    @Override
+    public boolean isGroupMembershipEditable() {
+        return true;
+    }
+
+    @Override
+    public boolean areContactsWritable() {
+        return true;
+    }
+}
diff --git a/src/com/android/contacts/common/model/account/ExternalAccountType.java b/src/com/android/contacts/common/model/account/ExternalAccountType.java
new file mode 100644
index 0000000..3bc5b3a
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/ExternalAccountType.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.google.common.annotations.VisibleForTesting;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A general contacts account type descriptor.
+ */
+public class ExternalAccountType extends BaseAccountType {
+    private static final String TAG = "ExternalAccountType";
+
+    private static final String METADATA_CONTACTS = "android.provider.CONTACTS_STRUCTURE";
+
+    private static final String TAG_CONTACTS_SOURCE_LEGACY = "ContactsSource";
+    private static final String TAG_CONTACTS_ACCOUNT_TYPE = "ContactsAccountType";
+    private static final String TAG_CONTACTS_DATA_KIND = "ContactsDataKind";
+    private static final String TAG_EDIT_SCHEMA = "EditSchema";
+
+    private static final String ATTR_EDIT_CONTACT_ACTIVITY = "editContactActivity";
+    private static final String ATTR_CREATE_CONTACT_ACTIVITY = "createContactActivity";
+    private static final String ATTR_INVITE_CONTACT_ACTIVITY = "inviteContactActivity";
+    private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel";
+    private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService";
+    private static final String ATTR_VIEW_GROUP_ACTIVITY = "viewGroupActivity";
+    private static final String ATTR_VIEW_GROUP_ACTION_LABEL = "viewGroupActionLabel";
+    private static final String ATTR_VIEW_STREAM_ITEM_ACTIVITY = "viewStreamItemActivity";
+    private static final String ATTR_VIEW_STREAM_ITEM_PHOTO_ACTIVITY =
+            "viewStreamItemPhotoActivity";
+    private static final String ATTR_DATA_SET = "dataSet";
+    private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames";
+
+    // The following attributes should only be set in non-sync-adapter account types.  They allow
+    // for the account type and resource IDs to be specified without an associated authenticator.
+    private static final String ATTR_ACCOUNT_TYPE = "accountType";
+    private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel";
+    private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon";
+
+    private final boolean mIsExtension;
+
+    private String mEditContactActivityClassName;
+    private String mCreateContactActivityClassName;
+    private String mInviteContactActivity;
+    private String mInviteActionLabelAttribute;
+    private int mInviteActionLabelResId;
+    private String mViewContactNotifyService;
+    private String mViewGroupActivity;
+    private String mViewGroupLabelAttribute;
+    private int mViewGroupLabelResId;
+    private String mViewStreamItemActivity;
+    private String mViewStreamItemPhotoActivity;
+    private List<String> mExtensionPackageNames;
+    private String mAccountTypeLabelAttribute;
+    private String mAccountTypeIconAttribute;
+    private boolean mHasContactsMetadata;
+    private boolean mHasEditSchema;
+
+    public ExternalAccountType(Context context, String resPackageName, boolean isExtension) {
+        this(context, resPackageName, isExtension, null);
+    }
+
+    /**
+     * Constructor used for testing to initialize with any arbitrary XML.
+     *
+     * @param injectedMetadata If non-null, it'll be used to initialize the type.  Only set by
+     *     tests.  If null, the metadata is loaded from the specified package.
+     */
+    ExternalAccountType(Context context, String packageName, boolean isExtension,
+            XmlResourceParser injectedMetadata) {
+        this.mIsExtension = isExtension;
+        this.resourcePackageName = packageName;
+        this.syncAdapterPackageName = packageName;
+
+        final PackageManager pm = context.getPackageManager();
+        final XmlResourceParser parser;
+        if (injectedMetadata == null) {
+            try {
+                parser = loadContactsXml(context, packageName);
+            } catch (NameNotFoundException e1) {
+                // If the package name is not found, we can't initialize this account type.
+                return;
+            }
+        } else {
+            parser = injectedMetadata;
+        }
+        boolean needLineNumberInErrorLog = true;
+        try {
+            if (parser != null) {
+                inflate(context, parser);
+            }
+
+            // Done parsing; line number no longer needed in error log.
+            needLineNumberInErrorLog = false;
+            if (mHasEditSchema) {
+                checkKindExists(StructuredName.CONTENT_ITEM_TYPE);
+                checkKindExists(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME);
+                checkKindExists(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME);
+                checkKindExists(Photo.CONTENT_ITEM_TYPE);
+            } else {
+                // Bring in name and photo from fallback source, which are non-optional
+                addDataKindStructuredName(context);
+                addDataKindDisplayName(context);
+                addDataKindPhoneticName(context);
+                addDataKindPhoto(context);
+            }
+        } catch (DefinitionException e) {
+            final StringBuilder error = new StringBuilder();
+            error.append("Problem reading XML");
+            if (needLineNumberInErrorLog && (parser != null)) {
+                error.append(" in line ");
+                error.append(parser.getLineNumber());
+            }
+            error.append(" for external package ");
+            error.append(packageName);
+
+            Log.e(TAG, error.toString(), e);
+            return;
+        } finally {
+            if (parser != null) {
+                parser.close();
+            }
+        }
+
+        mExtensionPackageNames = new ArrayList<String>();
+        mInviteActionLabelResId = resolveExternalResId(context, mInviteActionLabelAttribute,
+                syncAdapterPackageName, ATTR_INVITE_CONTACT_ACTION_LABEL);
+        mViewGroupLabelResId = resolveExternalResId(context, mViewGroupLabelAttribute,
+                syncAdapterPackageName, ATTR_VIEW_GROUP_ACTION_LABEL);
+        titleRes = resolveExternalResId(context, mAccountTypeLabelAttribute,
+                syncAdapterPackageName, ATTR_ACCOUNT_LABEL);
+        iconRes = resolveExternalResId(context, mAccountTypeIconAttribute,
+                syncAdapterPackageName, ATTR_ACCOUNT_ICON);
+
+        // If we reach this point, the account type has been successfully initialized.
+        mIsInitialized = true;
+    }
+
+    /**
+     * Returns the CONTACTS_STRUCTURE metadata (aka "contacts.xml") in the given apk package.
+     *
+     * Unfortunately, there's no public way to determine which service defines a sync service for
+     * which account type, so this method looks through all services in the package, and just
+     * returns the first CONTACTS_STRUCTURE metadata defined in any of them.
+     *
+     * Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata.  In this case
+     * the account type *will* be initialized with minimal configuration.
+     *
+     * On the other hand, if the package is not found, it throws a {@link NameNotFoundException},
+     * in which case the account type will *not* be initialized.
+     */
+    private XmlResourceParser loadContactsXml(Context context, String resPackageName)
+            throws NameNotFoundException {
+        final PackageManager pm = context.getPackageManager();
+        PackageInfo packageInfo = pm.getPackageInfo(resPackageName,
+                PackageManager.GET_SERVICES|PackageManager.GET_META_DATA);
+        for (ServiceInfo serviceInfo : packageInfo.services) {
+            final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm,
+                    METADATA_CONTACTS);
+            if (parser != null) {
+                return parser;
+            }
+        }
+        // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata.
+        return null;
+    }
+
+    private void checkKindExists(String mimeType) throws DefinitionException {
+        if (getKindForMimetype(mimeType) == null) {
+            throw new DefinitionException(mimeType + " must be supported");
+        }
+    }
+
+    @Override
+    public boolean isEmbedded() {
+        return false;
+    }
+
+    @Override
+    public boolean isExtension() {
+        return mIsExtension;
+    }
+
+    @Override
+    public boolean areContactsWritable() {
+        return mHasEditSchema;
+    }
+
+    /**
+     * Whether this account type has the android.provider.CONTACTS_STRUCTURE metadata xml.
+     */
+    public boolean hasContactsMetadata() {
+        return mHasContactsMetadata;
+    }
+
+    @Override
+    public String getEditContactActivityClassName() {
+        return mEditContactActivityClassName;
+    }
+
+    @Override
+    public String getCreateContactActivityClassName() {
+        return mCreateContactActivityClassName;
+    }
+
+    @Override
+    public String getInviteContactActivityClassName() {
+        return mInviteContactActivity;
+    }
+
+    @Override
+    protected int getInviteContactActionResId() {
+        return mInviteActionLabelResId;
+    }
+
+    @Override
+    public String getViewContactNotifyServiceClassName() {
+        return mViewContactNotifyService;
+    }
+
+    @Override
+    public String getViewGroupActivity() {
+        return mViewGroupActivity;
+    }
+
+    @Override
+    protected int getViewGroupLabelResId() {
+        return mViewGroupLabelResId;
+    }
+
+    @Override
+    public String getViewStreamItemActivity() {
+        return mViewStreamItemActivity;
+    }
+
+    @Override
+    public String getViewStreamItemPhotoActivity() {
+        return mViewStreamItemPhotoActivity;
+    }
+
+    @Override
+    public List<String> getExtensionPackageNames() {
+        return mExtensionPackageNames;
+    }
+
+    /**
+     * Inflate this {@link AccountType} from the given parser. This may only
+     * load details matching the publicly-defined schema.
+     */
+    protected void inflate(Context context, XmlPullParser parser) throws DefinitionException {
+        final AttributeSet attrs = Xml.asAttributeSet(parser);
+
+        try {
+            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");
+            }
+
+            String rootTag = parser.getName();
+            if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag) &&
+                    !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) {
+                throw new IllegalStateException("Top level element must be "
+                        + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag);
+            }
+
+            mHasContactsMetadata = true;
+
+            int attributeCount = parser.getAttributeCount();
+            for (int i = 0; i < attributeCount; i++) {
+                String attr = parser.getAttributeName(i);
+                String value = parser.getAttributeValue(i);
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, attr + "=" + value);
+                }
+                if (ATTR_EDIT_CONTACT_ACTIVITY.equals(attr)) {
+                    mEditContactActivityClassName = value;
+                } else if (ATTR_CREATE_CONTACT_ACTIVITY.equals(attr)) {
+                    mCreateContactActivityClassName = value;
+                } else if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) {
+                    mInviteContactActivity = value;
+                } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) {
+                    mInviteActionLabelAttribute = value;
+                } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) {
+                    mViewContactNotifyService = value;
+                } else if (ATTR_VIEW_GROUP_ACTIVITY.equals(attr)) {
+                    mViewGroupActivity = value;
+                } else if (ATTR_VIEW_GROUP_ACTION_LABEL.equals(attr)) {
+                    mViewGroupLabelAttribute = value;
+                } else if (ATTR_VIEW_STREAM_ITEM_ACTIVITY.equals(attr)) {
+                    mViewStreamItemActivity = value;
+                } else if (ATTR_VIEW_STREAM_ITEM_PHOTO_ACTIVITY.equals(attr)) {
+                    mViewStreamItemPhotoActivity = value;
+                } else if (ATTR_DATA_SET.equals(attr)) {
+                    dataSet = value;
+                } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) {
+                    mExtensionPackageNames.add(value);
+                } else if (ATTR_ACCOUNT_TYPE.equals(attr)) {
+                    accountType = value;
+                } else if (ATTR_ACCOUNT_LABEL.equals(attr)) {
+                    mAccountTypeLabelAttribute = value;
+                } else if (ATTR_ACCOUNT_ICON.equals(attr)) {
+                    mAccountTypeIconAttribute = value;
+                } else {
+                    Log.e(TAG, "Unsupported attribute " + attr);
+                }
+            }
+
+            // Parse all children kinds
+            final int startDepth = parser.getDepth();
+            while (((type = parser.next()) != XmlPullParser.END_TAG
+                        || parser.getDepth() > startDepth)
+                    && type != XmlPullParser.END_DOCUMENT) {
+
+                if (type != XmlPullParser.START_TAG || parser.getDepth() != startDepth + 1) {
+                    continue; // Not a direct child tag
+                }
+
+                String tag = parser.getName();
+                if (TAG_EDIT_SCHEMA.equals(tag)) {
+                    mHasEditSchema = true;
+                    parseEditSchema(context, parser, attrs);
+                } else if (TAG_CONTACTS_DATA_KIND.equals(tag)) {
+                    final TypedArray a = context.obtainStyledAttributes(attrs,
+                            android.R.styleable.ContactsDataKind);
+                    final DataKind kind = new DataKind();
+
+                    kind.mimeType = a
+                            .getString(com.android.internal.R.styleable.ContactsDataKind_mimeType);
+
+                    final String summaryColumn = a.getString(
+                            com.android.internal.R.styleable.ContactsDataKind_summaryColumn);
+                    if (summaryColumn != null) {
+                        // Inflate a specific column as summary when requested
+                        kind.actionHeader = new SimpleInflater(summaryColumn);
+                    }
+
+                    final String detailColumn = a.getString(
+                            com.android.internal.R.styleable.ContactsDataKind_detailColumn);
+                    final boolean detailSocialSummary = a.getBoolean(
+                            com.android.internal.R.styleable.ContactsDataKind_detailSocialSummary,
+                            false);
+
+                    if (detailSocialSummary) {
+                        // Inflate social summary when requested
+                        kind.actionBodySocial = true;
+                    }
+
+                    if (detailColumn != null) {
+                        // Inflate specific column as summary
+                        kind.actionBody = new SimpleInflater(detailColumn);
+                    }
+
+                    a.recycle();
+
+                    addKind(kind);
+                }
+            }
+        } catch (XmlPullParserException e) {
+            throw new DefinitionException("Problem reading XML", e);
+        } catch (IOException e) {
+            throw new DefinitionException("Problem reading XML", e);
+        }
+    }
+
+    /**
+     * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in
+     * the resource package.
+     *
+     * If the argument is in the invalid format or isn't a resource name, it returns -1.
+     *
+     * @param context context
+     * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel"
+     * @param packageName name of the package containing the resource.
+     * @param xmlAttributeName attribute name which the resource came from.  Used for logging.
+     */
+    @VisibleForTesting
+    static int resolveExternalResId(Context context, String resourceName,
+            String packageName, String xmlAttributeName) {
+        if (TextUtils.isEmpty(resourceName)) {
+            return -1; // Empty text is okay.
+        }
+        if (resourceName.charAt(0) != '@') {
+            Log.e(TAG, xmlAttributeName + " must be a resource name beginnig with '@'");
+            return -1;
+        }
+        final String name = resourceName.substring(1);
+        final Resources res;
+        try {
+             res = context.getPackageManager().getResourcesForApplication(packageName);
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "Unable to load package " + packageName);
+            return -1;
+        }
+        final int resId = res.getIdentifier(name, null, packageName);
+        if (resId == 0) {
+            Log.e(TAG, "Unable to load " + resourceName + " from package " + packageName);
+            return -1;
+        }
+        return resId;
+    }
+}
diff --git a/src/com/android/contacts/common/model/account/FallbackAccountType.java b/src/com/android/contacts/common/model/account/FallbackAccountType.java
new file mode 100644
index 0000000..71c23c2
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/FallbackAccountType.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.test.NeededForTesting;
+
+public class FallbackAccountType extends BaseAccountType {
+    private static final String TAG = "FallbackAccountType";
+
+    private FallbackAccountType(Context context, String resPackageName) {
+        this.accountType = null;
+        this.dataSet = null;
+        this.titleRes = R.string.account_phone;
+        this.iconRes = R.mipmap.ic_launcher_contacts;
+
+        // Note those are only set for unit tests.
+        this.resourcePackageName = resPackageName;
+        this.syncAdapterPackageName = resPackageName;
+
+        try {
+            addDataKindStructuredName(context);
+            addDataKindDisplayName(context);
+            addDataKindPhoneticName(context);
+            addDataKindNickname(context);
+            addDataKindPhone(context);
+            addDataKindEmail(context);
+            addDataKindStructuredPostal(context);
+            addDataKindIm(context);
+            addDataKindOrganization(context);
+            addDataKindPhoto(context);
+            addDataKindNote(context);
+            addDataKindWebsite(context);
+            addDataKindSipAddress(context);
+
+            mIsInitialized = true;
+        } catch (DefinitionException e) {
+            Log.e(TAG, "Problem building account type", e);
+        }
+    }
+
+    public FallbackAccountType(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Used to compare with an {@link ExternalAccountType} built from a test contacts.xml.
+     * In order to build {@link DataKind}s with the same resource package name,
+     * {@code resPackageName} is injectable.
+     */
+    @NeededForTesting
+    static AccountType createWithPackageNameForTest(Context context, String resPackageName) {
+        return new FallbackAccountType(context, resPackageName);
+    }
+
+    @Override
+    public boolean areContactsWritable() {
+        return true;
+    }
+}
diff --git a/src/com/android/contacts/common/model/account/GoogleAccountType.java b/src/com/android/contacts/common/model/account/GoogleAccountType.java
new file mode 100644
index 0000000..a5a2c57
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/GoogleAccountType.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+public class GoogleAccountType extends BaseAccountType {
+    private static final String TAG = "GoogleAccountType";
+
+    public static final String ACCOUNT_TYPE = "com.google";
+
+    private static final List<String> mExtensionPackages =
+            Lists.newArrayList("com.google.android.apps.plus");
+
+    public GoogleAccountType(Context context, String authenticatorPackageName) {
+        this.accountType = ACCOUNT_TYPE;
+        this.resourcePackageName = null;
+        this.syncAdapterPackageName = authenticatorPackageName;
+
+        try {
+            addDataKindStructuredName(context);
+            addDataKindDisplayName(context);
+            addDataKindPhoneticName(context);
+            addDataKindNickname(context);
+            addDataKindPhone(context);
+            addDataKindEmail(context);
+            addDataKindStructuredPostal(context);
+            addDataKindIm(context);
+            addDataKindOrganization(context);
+            addDataKindPhoto(context);
+            addDataKindNote(context);
+            addDataKindWebsite(context);
+            addDataKindSipAddress(context);
+            addDataKindGroupMembership(context);
+            addDataKindRelation(context);
+            addDataKindEvent(context);
+
+            mIsInitialized = true;
+        } catch (DefinitionException e) {
+            Log.e(TAG, "Problem building account type", e);
+        }
+    }
+
+    @Override
+    public List<String> getExtensionPackageNames() {
+        return mExtensionPackages;
+    }
+
+    @Override
+    protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+        final DataKind kind = super.addDataKindPhone(context);
+
+        kind.typeColumn = Phone.TYPE;
+        kind.typeList = Lists.newArrayList();
+        kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true)
+                .setCustomColumn(Phone.LABEL));
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+        final DataKind kind = super.addDataKindEmail(context);
+
+        kind.typeColumn = Email.TYPE;
+        kind.typeList = Lists.newArrayList();
+        kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+        kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+        kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+        kind.typeList.add(buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(
+                Email.LABEL));
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+        return kind;
+    }
+
+    private DataKind addDataKindRelation(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(Relation.CONTENT_ITEM_TYPE,
+                R.string.relationLabelsGroup, 160, true, R.layout.text_fields_editor_view));
+        kind.actionHeader = new RelationActionInflater();
+        kind.actionBody = new SimpleInflater(Relation.NAME);
+
+        kind.typeColumn = Relation.TYPE;
+        kind.typeList = Lists.newArrayList();
+        kind.typeList.add(buildRelationType(Relation.TYPE_ASSISTANT));
+        kind.typeList.add(buildRelationType(Relation.TYPE_BROTHER));
+        kind.typeList.add(buildRelationType(Relation.TYPE_CHILD));
+        kind.typeList.add(buildRelationType(Relation.TYPE_DOMESTIC_PARTNER));
+        kind.typeList.add(buildRelationType(Relation.TYPE_FATHER));
+        kind.typeList.add(buildRelationType(Relation.TYPE_FRIEND));
+        kind.typeList.add(buildRelationType(Relation.TYPE_MANAGER));
+        kind.typeList.add(buildRelationType(Relation.TYPE_MOTHER));
+        kind.typeList.add(buildRelationType(Relation.TYPE_PARENT));
+        kind.typeList.add(buildRelationType(Relation.TYPE_PARTNER));
+        kind.typeList.add(buildRelationType(Relation.TYPE_REFERRED_BY));
+        kind.typeList.add(buildRelationType(Relation.TYPE_RELATIVE));
+        kind.typeList.add(buildRelationType(Relation.TYPE_SISTER));
+        kind.typeList.add(buildRelationType(Relation.TYPE_SPOUSE));
+        kind.typeList.add(buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true)
+                .setCustomColumn(Relation.LABEL));
+
+        kind.defaultValues = new ContentValues();
+        kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup,
+                FLAGS_RELATION));
+
+        return kind;
+    }
+
+    private DataKind addDataKindEvent(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(Event.CONTENT_ITEM_TYPE,
+                    R.string.eventLabelsGroup, 150, true, R.layout.event_field_editor_view));
+        kind.actionHeader = new EventActionInflater();
+        kind.actionBody = new SimpleInflater(Event.START_DATE);
+
+        kind.typeColumn = Event.TYPE;
+        kind.typeList = Lists.newArrayList();
+        kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
+        kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
+        kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, true).setSpecificMax(1));
+        kind.typeList.add(buildEventType(Event.TYPE_ANNIVERSARY, false));
+        kind.typeList.add(buildEventType(Event.TYPE_OTHER, false));
+        kind.typeList.add(buildEventType(Event.TYPE_CUSTOM, false).setSecondary(true)
+                .setCustomColumn(Event.LABEL));
+
+        kind.defaultValues = new ContentValues();
+        kind.defaultValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
+
+        kind.fieldList = Lists.newArrayList();
+        kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+        return kind;
+    }
+
+    @Override
+    public boolean isGroupMembershipEditable() {
+        return true;
+    }
+
+    @Override
+    public boolean areContactsWritable() {
+        return true;
+    }
+
+    @Override
+    public String getViewContactNotifyServiceClassName() {
+        return "com.google.android.syncadapters.contacts." +
+                "SyncHighResPhotoIntentService";
+    }
+
+    @Override
+    public String getViewContactNotifyServicePackageName() {
+        return "com.google.android.syncadapters.contacts";
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/DataKind.java b/src/com/android/contacts/common/model/dataitem/DataKind.java
new file mode 100644
index 0000000..58a8e7b
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/DataKind.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.Data;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.account.AccountType.EditField;
+import com.android.contacts.common.model.account.AccountType.EditType;
+import com.android.contacts.common.model.account.AccountType.StringInflater;
+import com.google.common.collect.Iterators;
+
+import java.text.SimpleDateFormat;
+import java.util.List;
+
+/**
+ * Description of a specific data type, usually marked by a unique
+ * {@link Data#MIMETYPE}. Includes details about how to view and edit
+ * {@link Data} rows of this kind, including the possible {@link EditType}
+ * labels and editable {@link EditField}.
+ */
+public final class DataKind {
+
+    public static final String PSEUDO_MIME_TYPE_DISPLAY_NAME = "#displayName";
+    public static final String PSEUDO_MIME_TYPE_PHONETIC_NAME = "#phoneticName";
+    public static final String PSEUDO_COLUMN_PHONETIC_NAME = "#phoneticName";
+
+    public String resourcePackageName;
+    public String mimeType;
+    public int titleRes;
+    public int iconAltRes;
+    public int iconAltDescriptionRes;
+    public int weight;
+    public boolean editable;
+
+    public StringInflater actionHeader;
+    public StringInflater actionAltHeader;
+    public StringInflater actionBody;
+
+    public boolean actionBodySocial = false;
+
+    public String typeColumn;
+
+    /**
+     * Maximum number of values allowed in the list. -1 represents infinity.
+     */
+    public int typeOverallMax;
+
+    public List<EditType> typeList;
+    public List<EditField> fieldList;
+
+    public ContentValues defaultValues;
+
+    /** Layout resource id for an editor view to edit this {@link DataKind}. */
+    public final int editorLayoutResourceId;
+
+    /**
+     * If this is a date field, this specifies the format of the date when saving. The
+     * date includes year, month and day. If this is not a date field or the date field is not
+     * editable, this value should be ignored.
+     */
+    public SimpleDateFormat dateFormatWithoutYear;
+
+    /**
+     * If this is a date field, this specifies the format of the date when saving. The
+     * date includes month and day. If this is not a date field, the field is not editable or
+     * dates without year are not supported, this value should be ignored.
+     */
+    public SimpleDateFormat dateFormatWithYear;
+
+    /**
+     * The number of lines available for displaying this kind of data.
+     * Defaults to 1.
+     */
+    public int maxLinesForDisplay;
+
+    public DataKind() {
+        editorLayoutResourceId = R.layout.text_fields_editor_view;
+        maxLinesForDisplay = 1;
+    }
+
+    public DataKind(String mimeType, int titleRes, int weight, boolean editable,
+            int editorLayoutResourceId) {
+        this.mimeType = mimeType;
+        this.titleRes = titleRes;
+        this.weight = weight;
+        this.editable = editable;
+        this.typeOverallMax = -1;
+        this.editorLayoutResourceId = editorLayoutResourceId;
+        maxLinesForDisplay = 1;
+    }
+
+    public String getKindString(Context context) {
+        return (titleRes == -1 || titleRes == 0) ? "" : context.getString(titleRes);
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("DataKind:");
+        sb.append(" resPackageName=").append(resourcePackageName);
+        sb.append(" mimeType=").append(mimeType);
+        sb.append(" titleRes=").append(titleRes);
+        sb.append(" iconAltRes=").append(iconAltRes);
+        sb.append(" iconAltDescriptionRes=").append(iconAltDescriptionRes);
+        sb.append(" weight=").append(weight);
+        sb.append(" editable=").append(editable);
+        sb.append(" actionHeader=").append(actionHeader);
+        sb.append(" actionAltHeader=").append(actionAltHeader);
+        sb.append(" actionBody=").append(actionBody);
+        sb.append(" actionBodySocial=").append(actionBodySocial);
+        sb.append(" typeColumn=").append(typeColumn);
+        sb.append(" typeOverallMax=").append(typeOverallMax);
+        sb.append(" typeList=").append(toString(typeList));
+        sb.append(" fieldList=").append(toString(fieldList));
+        sb.append(" defaultValues=").append(defaultValues);
+        sb.append(" editorLayoutResourceId=").append(editorLayoutResourceId);
+        sb.append(" dateFormatWithoutYear=").append(toString(dateFormatWithoutYear));
+        sb.append(" dateFormatWithYear=").append(toString(dateFormatWithYear));
+
+        return sb.toString();
+    }
+
+    public static String toString(SimpleDateFormat format) {
+        return format == null ? "(null)" : format.toPattern();
+    }
+
+    public static String toString(Iterable<?> list) {
+        if (list == null) {
+            return "(null)";
+        } else {
+            return Iterators.toString(list.iterator());
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/test/NeededForTesting.java b/src/com/android/contacts/common/test/NeededForTesting.java
new file mode 100644
index 0000000..f82756a
--- /dev/null
+++ b/src/com/android/contacts/common/test/NeededForTesting.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.test;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that the class, constructor, method or field is used by tests and therefore cannot be
+ * removed by tools like ProGuard.
+ */
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD})
+public @interface NeededForTesting {}
diff --git a/src/com/android/contacts/common/util/CommonDateUtils.java b/src/com/android/contacts/common/util/CommonDateUtils.java
new file mode 100644
index 0000000..5dfd149
--- /dev/null
+++ b/src/com/android/contacts/common/util/CommonDateUtils.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.util;
+
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/**
+ * Common date utilities.
+ */
+public class CommonDateUtils {
+
+    // All the SimpleDateFormats in this class use the UTC timezone
+    public static final SimpleDateFormat NO_YEAR_DATE_FORMAT =
+            new SimpleDateFormat("--MM-dd", Locale.US);
+    public static final SimpleDateFormat FULL_DATE_FORMAT =
+            new SimpleDateFormat("yyyy-MM-dd", Locale.US);
+    public static final SimpleDateFormat DATE_AND_TIME_FORMAT =
+            new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+    public static final SimpleDateFormat NO_YEAR_DATE_AND_TIME_FORMAT =
+            new SimpleDateFormat("--MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index e181b2b..55a6059 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -20,6 +20,18 @@
 
     <application>
         <uses-library android:name="android.test.runner" />
+
+        <service android:name="com.android.contacts.common.tests.testauth.TestSyncService$Basic" android:exported="true">
+            <intent-filter>
+                <action android:name="android.content.SyncAdapter"/>
+            </intent-filter>
+            <meta-data
+                android:name="android.content.SyncAdapter"
+                android:resource="@xml/test_basic_syncadapter"/>
+            <meta-data
+                android:name="android.provider.CONTACTS_STRUCTURE"
+                android:resource="@xml/test_basic_contacts"/>
+        </service>
     </application>
 
     <instrumentation android:name="android.test.InstrumentationTestRunner"
diff --git a/tests/proguard.flags b/tests/proguard.flags
index 883dde6..d9d7942 100644
--- a/tests/proguard.flags
+++ b/tests/proguard.flags
@@ -1,8 +1,8 @@
 # Any class or method annotated with NeededForTesting or NeededForReflection.
--keep @com.android.contacts.test.NeededForTesting class *
+-keep @com.android.contacts.common.test.NeededForTesting class *
 -keep @com.android.contacts.test.NeededForReflection class *
 -keepclassmembers class * {
-@com.android.contacts.test.NeededForTesting *;
+@com.android.contacts.common.test.NeededForTesting *;
 @com.android.contacts.test.NeededForReflection *;
 }
 
diff --git a/tests/res/drawable/android.jpg b/tests/res/drawable/android.jpg
new file mode 100644
index 0000000..95693b2
--- /dev/null
+++ b/tests/res/drawable/android.jpg
Binary files differ
diff --git a/tests/res/drawable/default_icon.png b/tests/res/drawable/default_icon.png
new file mode 100644
index 0000000..cea0eb3
--- /dev/null
+++ b/tests/res/drawable/default_icon.png
Binary files differ
diff --git a/tests/res/drawable/ic_contact_picture.png b/tests/res/drawable/ic_contact_picture.png
new file mode 100644
index 0000000..6876777
--- /dev/null
+++ b/tests/res/drawable/ic_contact_picture.png
Binary files differ
diff --git a/tests/res/drawable/phone_icon.png b/tests/res/drawable/phone_icon.png
new file mode 100644
index 0000000..4e613ec
--- /dev/null
+++ b/tests/res/drawable/phone_icon.png
Binary files differ
diff --git a/tests/res/values/donottranslate_strings.xml b/tests/res/values/donottranslate_strings.xml
new file mode 100644
index 0000000..6c8527f
--- /dev/null
+++ b/tests/res/values/donottranslate_strings.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<resources>
+    <string name="test_string">TEST STRING</string>
+
+    <string name="authenticator_basic_label">Test adapter</string>
+</resources>
diff --git a/tests/res/xml/contacts_fallback.xml b/tests/res/xml/contacts_fallback.xml
new file mode 100644
index 0000000..ae262eb
--- /dev/null
+++ b/tests/res/xml/contacts_fallback.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!--
+    contacts.xml to build "fallback account type" equivalent.
+    This is directly used in ExternalAccountTypeTest to test the parser.  There's no sync adapter
+    that actually defined with this definition.
+-->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema
+        >
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsDisplayName="true"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="true"
+            supportsPhoneticGivenName="true"
+            >
+        </DataKind>
+        <DataKind kind="photo" maxOccurs="1" />
+        <DataKind kind="phone" >
+            <Type type="mobile" />
+            <Type type="home" />
+            <Type type="work" />
+            <Type type="fax_work" />
+            <Type type="fax_home" />
+            <Type type="pager" />
+            <Type type="other" />
+            <Type type="custom"/>
+            <Type type="callback" />
+            <Type type="car" />
+            <Type type="company_main" />
+            <Type type="isdn" />
+            <Type type="main" />
+            <Type type="other_fax" />
+            <Type type="radio" />
+            <Type type="telex" />
+            <Type type="tty_tdd" />
+            <Type type="work_mobile"/>
+            <Type type="work_pager" />
+            <Type type="assistant" />
+            <Type type="mms" />
+        </DataKind>
+        <DataKind kind="email" >
+            <Type type="home" />
+            <Type type="work" />
+            <Type type="other" />
+            <Type type="mobile" />
+            <Type type="custom" />
+        </DataKind>
+        <DataKind kind="nickname" maxOccurs="1" />
+        <DataKind kind="im" >
+            <Type type="aim" />
+            <Type type="msn" />
+            <Type type="yahoo" />
+            <Type type="skype" />
+            <Type type="qq" />
+            <Type type="google_talk" />
+            <Type type="icq" />
+            <Type type="jabber" />
+            <Type type="custom" />
+        </DataKind>
+        <DataKind kind="postal" needsStructured="false" >
+            <Type type="home" />
+            <Type type="work" />
+            <Type type="other" />
+            <Type type="custom" />
+        </DataKind>
+        <DataKind kind="organization" maxOccurs="1" />
+        <DataKind kind="website" />
+        <DataKind kind="sip_address" maxOccurs="1" />
+        <DataKind kind="note" maxOccurs="1" />
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/contacts_readonly.xml b/tests/res/xml/contacts_readonly.xml
new file mode 100644
index 0000000..df8d9c0
--- /dev/null
+++ b/tests/res/xml/contacts_readonly.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!--
+    Contacts.xml without EditSchema.
+-->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+        <ContactsDataKind
+                android:icon="@drawable/android"
+                android:mimeType="vnd.android.cursor.item/a.b.c"
+                android:summaryColumn="data1"
+                android:detailColumn="data2"
+                android:detailSocialSummary="true"
+                >
+        </ContactsDataKind>
+        <ContactsDataKind
+                android:icon="@drawable/default_icon"
+                android:mimeType="vnd.android.cursor.item/d.e.f"
+                android:summaryColumn="data3"
+                android:detailColumn="data4"
+                android:detailSocialSummary="false"
+                >
+        </ContactsDataKind>
+        <ContactsDataKind
+                android:icon="@drawable/android"
+                android:mimeType="vnd.android.cursor.item/xyz"
+                android:summaryColumn="data5"
+                android:detailColumn="data6"
+                android:detailSocialSummary="true"
+                >
+        </ContactsDataKind>
+</ContactsAccountType>
diff --git a/tests/res/xml/iconset.xml b/tests/res/xml/iconset.xml
new file mode 100644
index 0000000..d1207e7
--- /dev/null
+++ b/tests/res/xml/iconset.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<icon-set
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <icon-default android:icon="@drawable/default_icon" />
+    <icon android:mimeType="vnd.android.cursor.item/phone" 
+        android:icon="@drawable/phone_icon" />
+
+</icon-set>
\ No newline at end of file
diff --git a/tests/res/xml/missing_contacts_base.xml b/tests/res/xml/missing_contacts_base.xml
new file mode 100644
index 0000000..2c9aa6d
--- /dev/null
+++ b/tests/res/xml/missing_contacts_base.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- XML for must-have checks.  Base definition, which is valid. -->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema>
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsDisplayName="true"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="true"
+            supportsPhoneticGivenName="true"
+            >
+        </DataKind>
+        <DataKind kind="photo" maxOccurs="1" />
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/missing_contacts_name.xml b/tests/res/xml/missing_contacts_name.xml
new file mode 100644
index 0000000..1ac26be
--- /dev/null
+++ b/tests/res/xml/missing_contacts_name.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- XML for must-have checks.  Missing "name" kind. -->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema>
+        <DataKind kind="photo" maxOccurs="1" />
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/missing_contacts_name_attr1.xml b/tests/res/xml/missing_contacts_name_attr1.xml
new file mode 100644
index 0000000..b7b0f19
--- /dev/null
+++ b/tests/res/xml/missing_contacts_name_attr1.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- XML for must-have checks.  Missing one of the "support*" attributes". -->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema>
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="true"
+            supportsPhoneticGivenName="true"
+            />
+        <DataKind kind="photo" maxOccurs="1" />
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/missing_contacts_name_attr2.xml b/tests/res/xml/missing_contacts_name_attr2.xml
new file mode 100644
index 0000000..41be9e8
--- /dev/null
+++ b/tests/res/xml/missing_contacts_name_attr2.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- XML for must-have checks.  Missing one of the "support*" attributes". -->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema>
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsDisplayName="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="true"
+            supportsPhoneticGivenName="true"
+            />
+        <DataKind kind="photo" maxOccurs="1" />
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/missing_contacts_name_attr3.xml b/tests/res/xml/missing_contacts_name_attr3.xml
new file mode 100644
index 0000000..e639a76
--- /dev/null
+++ b/tests/res/xml/missing_contacts_name_attr3.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- XML for must-have checks.  Missing one of the "support*" attributes". -->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema>
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsDisplayName="true"
+            supportsPrefix="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="true"
+            supportsPhoneticGivenName="true"
+            />
+        <DataKind kind="photo" maxOccurs="1" />
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/missing_contacts_name_attr4.xml b/tests/res/xml/missing_contacts_name_attr4.xml
new file mode 100644
index 0000000..b42cdcd
--- /dev/null
+++ b/tests/res/xml/missing_contacts_name_attr4.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- XML for must-have checks.  Missing one of the "support*" attributes". -->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema>
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsDisplayName="true"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="true"
+            supportsPhoneticGivenName="true"
+            />
+        <DataKind kind="photo" maxOccurs="1" />
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/missing_contacts_name_attr5.xml b/tests/res/xml/missing_contacts_name_attr5.xml
new file mode 100644
index 0000000..3778d2f
--- /dev/null
+++ b/tests/res/xml/missing_contacts_name_attr5.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- XML for must-have checks.  Missing one of the "support*" attributes". -->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema>
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsDisplayName="true"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticMiddleName="true"
+            supportsPhoneticGivenName="true"
+            />
+        <DataKind kind="photo" maxOccurs="1" />
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/missing_contacts_name_attr6.xml b/tests/res/xml/missing_contacts_name_attr6.xml
new file mode 100644
index 0000000..b3a3411
--- /dev/null
+++ b/tests/res/xml/missing_contacts_name_attr6.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- XML for must-have checks.  Missing one of the "support*" attributes". -->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema>
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsDisplayName="true"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticGivenName="true"
+            />
+        <DataKind kind="photo" maxOccurs="1" />
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/missing_contacts_name_attr7.xml b/tests/res/xml/missing_contacts_name_attr7.xml
new file mode 100644
index 0000000..c87e4f1
--- /dev/null
+++ b/tests/res/xml/missing_contacts_name_attr7.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- XML for must-have checks.  Missing one of the "support*" attributes". -->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema>
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsDisplayName="true"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="true"
+            />
+        <DataKind kind="photo" maxOccurs="1" />
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/missing_contacts_photo.xml b/tests/res/xml/missing_contacts_photo.xml
new file mode 100644
index 0000000..87f4fc6
--- /dev/null
+++ b/tests/res/xml/missing_contacts_photo.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- XML for must-have checks.  Missing "photo" kind. -->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema>
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsDisplayName="true"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="true"
+            supportsPhoneticGivenName="true"
+            >
+        </DataKind>
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/test_basic_contacts.xml b/tests/res/xml/test_basic_contacts.xml
new file mode 100644
index 0000000..0047204
--- /dev/null
+++ b/tests/res/xml/test_basic_contacts.xml
@@ -0,0 +1,283 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema
+        >
+        <!--
+            Name:
+            - maxOccurs must be 1
+            - No types.
+
+            - Currently all the supportsXxx attributes must be true, but here's the plan for the
+              future:
+              (There's some hardcoded assumptions in the contact editor, which is one reason
+              for the above restriction)
+
+                - "Family name" and "Given name" must be supported.
+                    - All sync adapters must support structured name. "display name only" is not
+                      supported.
+                      -> Supporting this would require relatively large changes to
+                         the contact editor.
+
+                - Fields are decided from the attributes:
+                    StructuredName.DISPLAY_NAME         if supportsDisplayName == true
+                    StructuredName.PREFIX               if supportsPrefix == true
+                    StructuredName.FAMILY_NAME          (always)
+                    StructuredName.MIDDLE_NAME          if supportsPrefix == true
+                    StructuredName.GIVEN_NAME           (always)
+                    StructuredName.SUFFIX               if supportsSuffix == true
+                    StructuredName.PHONETIC_FAMILY_NAME if supportsPhoneticFamilyName == true
+                    StructuredName.PHONETIC_MIDDLE_NAME if supportsPhoneticMiddleName == true
+                    StructuredName.PHONETIC_GIVEN_NAME  if supportsPhoneticGivenName == true
+
+                - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME  is always added.
+                - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME is added
+                  if any of supportsPhoneticXxx == true
+        -->
+        <!-- Fallback/Google definition.  Supports all. -->
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsDisplayName="true"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="true"
+            supportsPhoneticGivenName="true"
+            >
+        </DataKind>
+
+        <!-- Exchange definition.  No display-name, no phonetic-middle.
+        <DataKind kind="name"
+            supportsDisplayName="false"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="false"
+            supportsPhoneticGivenName ="true"
+            >
+        </DataKind>
+        -->
+
+        <!--
+            Photo:
+            - maxOccurs must be 1
+            - No types.
+        -->
+        <DataKind kind="photo" maxOccurs="1" />
+
+        <!--
+            Phone definition.
+            - "is secondary?" is inferred from type.
+        -->
+        <!-- Fallback, Google definition.  -->
+        <DataKind kind="phone" >
+            <!-- Note: Google type doesn't have obsolete ones -->
+            <Type type="mobile" />
+            <Type type="home" />
+            <Type type="work" />
+            <Type type="fax_work" />
+            <Type type="fax_home" />
+            <Type type="pager" />
+            <Type type="other" />
+            <Type type="custom"/>
+            <Type type="callback" />
+            <Type type="car" />
+            <Type type="company_main" />
+            <Type type="isdn" />
+            <Type type="main" />
+            <Type type="other_fax" />
+            <Type type="radio" />
+            <Type type="telex" />
+            <Type type="tty_tdd" />
+            <Type type="work_mobile"/>
+            <Type type="work_pager" />
+            <Type type="assistant" />
+            <Type type="mms" />
+        </DataKind>
+
+        <!-- Exchange definition.
+        <DataKind kind="phone" >
+            <Type type="home" maxOccurs="2" />
+            <Type type="mobile" maxOccurs="1" />
+            <Type type="work" maxOccurs="2" />
+            <Type type="fax_work" maxOccurs="1" />
+            <Type type="fax_home" maxOccurs="1" />
+            <Type type="pager" maxOccurs="1" />
+            <Type type="car" maxOccurs="1" />
+            <Type type="company_main" maxOccurs="1" />
+            <Type type="mms" maxOccurs="1" />
+            <Type type="radio" maxOccurs="1" />
+            <Type type="assistant" maxOccurs="1" />
+        </DataKind>
+        -->
+
+        <!--
+            Email
+        -->
+        <!-- Fallback/Google definition.  -->
+        <DataKind kind="email" >
+            <!-- Note: Google type doesn't have obsolete ones -->
+            <Type type="home" />
+            <Type type="work" />
+            <Type type="other" />
+            <Type type="mobile" />
+            <Type type="custom" />
+        </DataKind>
+
+        <!--
+            Exchange definition.
+            - Same definition as "fallback" except for maxOccurs=3
+        <DataKind kind="email" maxOccurs="3" >
+            <Type type="home" />
+            <Type type="work" />
+            <Type type="other" />
+            <Type type="mobile" />
+            <Type type="custom" />
+        </DataKind>
+        -->
+
+        <!--
+            Nickname
+            - maxOccurs must be 1
+            - No types.
+        -->
+        <DataKind kind="nickname" maxOccurs="1" />
+
+        <!--
+            Im:
+             - The TYPE column always stores Im.TYPE_OTHER (defaultValues is always set)
+             - The user-selected type is stored in Im.PROTOCOL
+        -->
+        <!-- Fallback, Google definition.  -->
+        <DataKind kind="im" >
+            <Type type="aim" />
+            <Type type="msn" />
+            <Type type="yahoo" />
+            <Type type="skype" />
+            <Type type="qq" />
+            <Type type="google_talk" />
+            <Type type="icq" />
+            <Type type="jabber" />
+            <Type type="custom" />
+        </DataKind>
+
+        <!-- Exchange definition.
+        <DataKind kind="im" maxOccurs="3" >
+            <Type type="aim" />
+            <Type type="msn" />
+            <Type type="yahoo" />
+            <Type type="skype" />
+            <Type type="qq" />
+            <Type type="google_talk" />
+            <Type type="icq" />
+            <Type type="jabber" />
+            <Type type="custom" />
+        </DataKind>
+        -->
+
+        <!--
+            Postal address.
+        -->
+        <!-- Fallback/Google definition.  Not structured. -->
+        <DataKind kind="postal" needsStructured="false" >
+            <Type type="home" />
+            <Type type="work" />
+            <Type type="other" />
+            <Type type="custom" />
+        </DataKind>
+
+        <!-- Exchange definition.  Structured.
+        <DataKind kind="postal" needsStructured="true" >
+            <Type type="work" />
+            <Type type="home" />
+            <Type type="other" />
+        </DataKind>
+        -->
+
+        <!--
+            Organization:
+            - Fields are fixed: COMPANY, TITLE
+            - maxOccurs must be 1
+            - No types.
+        -->
+        <DataKind kind="organization" maxOccurs="1" />
+
+        <!--
+            Website:
+            - No types.
+        -->
+        <DataKind kind="website" />
+
+        <!--
+            Below kinds have nothing configurable.
+            - No types are supported.
+            - maxOccurs must be 1
+        -->
+        <DataKind kind="sip_address" maxOccurs="1" />
+        <DataKind kind="note" maxOccurs="1" />
+
+        <!--
+            Google/Exchange supports it, but fallback doesn't.
+        <DataKind kind="group_membership" maxOccurs="1" />
+        -->
+
+        <!--
+            Event
+        -->
+        <DataKind kind="event" dateWithTime="false">
+            <Type type="birthday" maxOccurs="1" yearOptional="true" />
+            <Type type="anniversary" />
+            <Type type="other" />
+            <Type type="custom" />
+        </DataKind>
+
+        <!--
+            Exchange definition.  dateWithTime is needed only for Exchange.
+        <DataKind kind="event" dateWithTime="true">
+            <Type type="birthday" maxOccurs="1" />
+        </DataKind>
+        -->
+
+        <!--
+            Relationship
+        -->
+        <DataKind kind="relationship" >
+            <Type type="assistant" />
+            <Type type="brother" />
+            <Type type="child" />
+            <Type type="domestic_partner" />
+            <Type type="father" />
+            <Type type="friend" />
+            <Type type="manager" />
+            <Type type="mother" />
+            <Type type="parent" />
+            <Type type="partner" />
+            <Type type="referred_by" />
+            <Type type="relative" />
+            <Type type="sister" />
+            <Type type="spouse" />
+            <Type type="custom" />
+        </DataKind>
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/test_basic_syncadapter.xml b/tests/res/xml/test_basic_syncadapter.xml
new file mode 100644
index 0000000..fecc0eb
--- /dev/null
+++ b/tests/res/xml/test_basic_syncadapter.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:contentAuthority="com.android.contacts"
+    android:accountType="com.android.contacts.tests.authtest.basic"
+    android:supportsUploading="true"
+    android:userVisible="true"
+/>
diff --git a/tests/src/com/android/contacts/common/model/AccountWithDataSetTest.java b/tests/src/com/android/contacts/common/model/AccountWithDataSetTest.java
new file mode 100644
index 0000000..e28f09e
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/AccountWithDataSetTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.os.Bundle;
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * Test case for {@link AccountWithDataSet}.
+ *
+ * adb shell am instrument -w -e class com.android.contacts.model.AccountWithDataSetTest \
+       com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+@SmallTest
+public class AccountWithDataSetTest extends AndroidTestCase {
+    public void testStringifyAndUnstringify() {
+        AccountWithDataSet a1 = new AccountWithDataSet("name1", "typeA", null);
+        AccountWithDataSet a2 = new AccountWithDataSet("name2", "typeB", null);
+        AccountWithDataSet a3 = new AccountWithDataSet("name3", "typeB", "dataset");
+
+        // stringify() & unstringify
+        AccountWithDataSet a1r = AccountWithDataSet.unstringify(a1.stringify());
+        AccountWithDataSet a2r = AccountWithDataSet.unstringify(a2.stringify());
+        AccountWithDataSet a3r = AccountWithDataSet.unstringify(a3.stringify());
+
+        assertEquals(a1, a1r);
+        assertEquals(a2, a2r);
+        assertEquals(a3, a3r);
+
+        MoreAsserts.assertNotEqual(a1, a2r);
+        MoreAsserts.assertNotEqual(a1, a3r);
+
+        MoreAsserts.assertNotEqual(a2, a1r);
+        MoreAsserts.assertNotEqual(a2, a3r);
+
+        MoreAsserts.assertNotEqual(a3, a1r);
+        MoreAsserts.assertNotEqual(a3, a2r);
+    }
+
+    public void testStringifyListAndUnstringify() {
+        AccountWithDataSet a1 = new AccountWithDataSet("name1", "typeA", null);
+        AccountWithDataSet a2 = new AccountWithDataSet("name2", "typeB", null);
+        AccountWithDataSet a3 = new AccountWithDataSet("name3", "typeB", "dataset");
+
+        // Empty list
+        assertEquals(0, stringifyListAndUnstringify().size());
+
+        // 1 element
+        final List<AccountWithDataSet> listA = stringifyListAndUnstringify(a1);
+        assertEquals(1, listA.size());
+        assertEquals(a1, listA.get(0));
+
+        // 2 elements
+        final List<AccountWithDataSet> listB = stringifyListAndUnstringify(a2, a1);
+        assertEquals(2, listB.size());
+        assertEquals(a2, listB.get(0));
+        assertEquals(a1, listB.get(1));
+
+        // 3 elements
+        final List<AccountWithDataSet> listC = stringifyListAndUnstringify(a3, a2, a1);
+        assertEquals(3, listC.size());
+        assertEquals(a3, listC.get(0));
+        assertEquals(a2, listC.get(1));
+        assertEquals(a1, listC.get(2));
+    }
+
+    private static List<AccountWithDataSet> stringifyListAndUnstringify(
+            AccountWithDataSet... accounts) {
+
+        List<AccountWithDataSet> list = Lists.newArrayList(accounts);
+        return AccountWithDataSet.unstringifyList(AccountWithDataSet.stringifyList(list));
+    }
+
+    public void testParcelable() {
+        AccountWithDataSet a1 = new AccountWithDataSet("name1", "typeA", null);
+        AccountWithDataSet a2 = new AccountWithDataSet("name2", "typeB", null);
+        AccountWithDataSet a3 = new AccountWithDataSet("name3", "typeB", "dataset");
+
+        // Parcel them & unpercel.
+        final Bundle b = new Bundle();
+        b.putParcelable("a1", a1);
+        b.putParcelable("a2", a2);
+        b.putParcelable("a3", a3);
+
+        AccountWithDataSet a1r = b.getParcelable("a1");
+        AccountWithDataSet a2r = b.getParcelable("a2");
+        AccountWithDataSet a3r = b.getParcelable("a3");
+
+        assertEquals(a1, a1r);
+        assertEquals(a2, a2r);
+        assertEquals(a3, a3r);
+
+        MoreAsserts.assertNotEqual(a1, a2r);
+        MoreAsserts.assertNotEqual(a1, a3r);
+
+        MoreAsserts.assertNotEqual(a2, a1r);
+        MoreAsserts.assertNotEqual(a2, a3r);
+
+        MoreAsserts.assertNotEqual(a3, a1r);
+        MoreAsserts.assertNotEqual(a3, a2r);
+    }
+}
diff --git a/tests/src/com/android/contacts/common/model/account/AccountTypeTest.java b/tests/src/com/android/contacts/common/model/account/AccountTypeTest.java
new file mode 100644
index 0000000..4374ad3
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/account/AccountTypeTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.contacts.common.unittest.R;
+
+/**
+ * Test case for {@link AccountType}.
+ *
+ * adb shell am instrument -w -e class com.android.contacts.model.AccountTypeTest \
+       com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+@SmallTest
+public class AccountTypeTest extends AndroidTestCase {
+    public void testGetResourceText() {
+        // In this test we use the test package itself as an external package.
+        final String packageName = getTestContext().getPackageName();
+
+        final Context c = getContext();
+        final String DEFAULT = "ABC";
+
+        // Package name null, resId -1, use the default
+        assertEquals(DEFAULT, AccountType.getResourceText(c, null, -1, DEFAULT));
+
+        // Resource ID -1, use the default
+        assertEquals(DEFAULT, AccountType.getResourceText(c, packageName, -1, DEFAULT));
+
+        // Load from an external package.  (here, we use this test package itself)
+        final int externalResID = R.string.test_string;
+        assertEquals(getTestContext().getString(externalResID),
+                AccountType.getResourceText(c, packageName, externalResID, DEFAULT));
+
+        // Load from the contacts package itself.
+        final int internalResId = com.android.contacts.common.R.string.contactsList;
+        assertEquals(c.getString(internalResId),
+                AccountType.getResourceText(c, null, internalResId, DEFAULT));
+    }
+
+    /**
+     * Verify if {@link AccountType#getInviteContactActionLabel} correctly gets the resource ID
+     * from {@link AccountType#getInviteContactActionResId}
+     */
+    public void testGetInviteContactActionLabel() {
+        final String packageName = getTestContext().getPackageName();
+        final Context c = getContext();
+
+        final int externalResID = R.string.test_string;
+
+        AccountType accountType = new AccountType() {
+            {
+                resourcePackageName = packageName;
+                syncAdapterPackageName = packageName;
+            }
+            @Override protected int getInviteContactActionResId() {
+                return externalResID;
+            }
+
+            @Override public boolean isGroupMembershipEditable() {
+                return false;
+            }
+
+            @Override public boolean areContactsWritable() {
+                return false;
+            }
+        };
+
+        assertEquals(getTestContext().getString(externalResID),
+                accountType.getInviteContactActionLabel(c));
+    }
+
+    public void testDisplayLabelComparator() {
+        final AccountTypeForDisplayLabelTest EMPTY = new AccountTypeForDisplayLabelTest("");
+        final AccountTypeForDisplayLabelTest NULL = new AccountTypeForDisplayLabelTest(null);
+        final AccountTypeForDisplayLabelTest AA = new AccountTypeForDisplayLabelTest("aa");
+        final AccountTypeForDisplayLabelTest BBB = new AccountTypeForDisplayLabelTest("bbb");
+        final AccountTypeForDisplayLabelTest C = new AccountTypeForDisplayLabelTest("c");
+
+        assertTrue(compareDisplayLabel(AA, BBB) < 0);
+        assertTrue(compareDisplayLabel(BBB, C) < 0);
+        assertTrue(compareDisplayLabel(AA, C) < 0);
+        assertTrue(compareDisplayLabel(AA, AA) == 0);
+        assertTrue(compareDisplayLabel(BBB, AA) > 0);
+
+        assertTrue(compareDisplayLabel(EMPTY, AA) < 0);
+        assertTrue(compareDisplayLabel(EMPTY, NULL) == 0);
+    }
+
+    private int compareDisplayLabel(AccountType lhs, AccountType rhs) {
+        return new AccountType.DisplayLabelComparator(getContext()).compare(lhs, rhs);
+    }
+
+    private class AccountTypeForDisplayLabelTest extends AccountType {
+        private final String mDisplayLabel;
+
+        public AccountTypeForDisplayLabelTest(String displayLabel) {
+            mDisplayLabel = displayLabel;
+        }
+
+        @Override
+        public CharSequence getDisplayLabel(Context context) {
+            return mDisplayLabel;
+        }
+
+        @Override
+        public boolean isGroupMembershipEditable() {
+            return false;
+        }
+
+        @Override
+        public boolean areContactsWritable() {
+            return false;
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/common/model/account/ExternalAccountTypeTest.java b/tests/src/com/android/contacts/common/model/account/ExternalAccountTypeTest.java
new file mode 100644
index 0000000..56ee832
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/account/ExternalAccountTypeTest.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.unittest.R;
+import com.google.common.base.Objects;
+
+import java.util.List;
+
+/**
+ * Test case for {@link com.android.contacts.common.model.account.ExternalAccountType}.
+ *
+ * adb shell am instrument -w -e class com.android.contacts.model.ExternalAccountTypeTest \
+       com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+@SmallTest
+public class ExternalAccountTypeTest extends AndroidTestCase {
+    public void testResolveExternalResId() {
+        final Context c = getContext();
+        // In this test we use the test package itself as an external package.
+        final String packageName = getTestContext().getPackageName();
+
+        // Resource name empty.
+        assertEquals(-1, ExternalAccountType.resolveExternalResId(c, null, packageName, ""));
+        assertEquals(-1, ExternalAccountType.resolveExternalResId(c, "", packageName, ""));
+
+        // Name doesn't begin with '@'
+        assertEquals(-1, ExternalAccountType.resolveExternalResId(c, "x", packageName, ""));
+
+        // Invalid resource name
+        assertEquals(-1, ExternalAccountType.resolveExternalResId(c, "@", packageName, ""));
+        assertEquals(-1, ExternalAccountType.resolveExternalResId(c, "@a", packageName, ""));
+        assertEquals(-1, ExternalAccountType.resolveExternalResId(c, "@a/b", packageName, ""));
+
+        // Valid resource name
+        assertEquals(R.string.test_string, ExternalAccountType.resolveExternalResId(c,
+                "@string/test_string", packageName, ""));
+    }
+
+    /**
+     * Initialize with an invalid package name and see if type type will *not* be initialized.
+     */
+    public void testNoPackage() {
+        final ExternalAccountType type = new ExternalAccountType(getContext(),
+                "!!!no such package name!!!", false);
+        assertFalse(type.isInitialized());
+    }
+
+    /**
+     * Initialize with the name of an existing package, which has no contacts.xml metadata.
+     */
+    /*
+    public void testNoMetadata() {
+        // Use the main application package, which does exist, but has no contacts.xml in it.
+        String packageName = getContext().getPackageName();
+        Log.e("TEST", packageName);
+        final ExternalAccountType type = new ExternalAccountType(getContext(),
+                packageName, false);
+        assertTrue(type.isInitialized());
+    }
+    */
+
+    /**
+     * Initialize with the test package itself and see if EditSchema is correctly parsed.
+     */
+    public void testEditSchema() {
+        final ExternalAccountType type = new ExternalAccountType(getContext(),
+                getTestContext().getPackageName(), false);
+
+        assertTrue(type.isInitialized());
+
+        // Let's just check if the DataKinds are registered.
+        assertNotNull(type.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME));
+        assertNotNull(type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME));
+        assertNotNull(type.getKindForMimetype(Email.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Im.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Organization.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Photo.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Note.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Website.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(SipAddress.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Event.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Relation.CONTENT_ITEM_TYPE));
+    }
+
+    /**
+     * Initialize with "contacts_fallback.xml" and compare the DataKinds to those of
+     * {@link com.android.contacts.common.model.account.FallbackAccountType}.
+     */
+    public void testEditSchema_fallback() {
+        final ExternalAccountType type = new ExternalAccountType(getContext(),
+                getTestContext().getPackageName(), false,
+                getTestContext().getResources().getXml(R.xml.contacts_fallback)
+                );
+
+        assertTrue(type.isInitialized());
+
+        // Create a fallback type with the same resource package name, and compare all the data
+        // kinds to its.
+        final AccountType reference = FallbackAccountType.createWithPackageNameForTest(
+                getContext(), type.resourcePackageName);
+
+        assertsDataKindEquals(reference.getSortedDataKinds(), type.getSortedDataKinds());
+    }
+
+    public void testEditSchema_mustHaveChecks() {
+        checkEditSchema_mustHaveChecks(R.xml.missing_contacts_base, true);
+        checkEditSchema_mustHaveChecks(R.xml.missing_contacts_photo, false);
+        checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name, false);
+        checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr1, false);
+        checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr2, false);
+        checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr3, false);
+        checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr4, false);
+        checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr5, false);
+        checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr6, false);
+        checkEditSchema_mustHaveChecks(R.xml.missing_contacts_name_attr7, false);
+    }
+
+    private void checkEditSchema_mustHaveChecks(int xmlResId, boolean expectInitialized) {
+        final ExternalAccountType type = new ExternalAccountType(getContext(),
+                getTestContext().getPackageName(), false,
+                getTestContext().getResources().getXml(xmlResId)
+                );
+
+        assertEquals(expectInitialized, type.isInitialized());
+    }
+
+    /**
+     * Initialize with "contacts_readonly.xml" and see if all data kinds are correctly registered.
+     */
+    public void testReadOnlyDefinition() {
+        final ExternalAccountType type = new ExternalAccountType(getContext(),
+                getTestContext().getPackageName(), false,
+                getTestContext().getResources().getXml(R.xml.contacts_readonly)
+                );
+        assertTrue(type.isInitialized());
+
+        // Shouldn't have a "null" mimetype.
+        assertTrue(type.getKindForMimetype(null) == null);
+
+        // 3 kinds are defined in XML and 4 are added by default.
+        assertEquals(4 + 3, type.getSortedDataKinds().size());
+
+        // Check for the default kinds.
+        assertNotNull(type.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME));
+        assertNotNull(type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME));
+        assertNotNull(type.getKindForMimetype(Photo.CONTENT_ITEM_TYPE));
+
+        // Check for type specific kinds.
+        DataKind kind = type.getKindForMimetype("vnd.android.cursor.item/a.b.c");
+        assertNotNull(kind);
+        // No check for icon -- we actually just ignore it.
+        assertEquals("data1", ((BaseAccountType.SimpleInflater) kind.actionHeader)
+                .getColumnNameForTest());
+        assertEquals("data2", ((BaseAccountType.SimpleInflater) kind.actionBody)
+                .getColumnNameForTest());
+        assertEquals(true, kind.actionBodySocial);
+
+        kind = type.getKindForMimetype("vnd.android.cursor.item/d.e.f");
+        assertNotNull(kind);
+        assertEquals("data3", ((BaseAccountType.SimpleInflater) kind.actionHeader)
+                .getColumnNameForTest());
+        assertEquals("data4", ((BaseAccountType.SimpleInflater) kind.actionBody)
+                .getColumnNameForTest());
+        assertEquals(false, kind.actionBodySocial);
+
+        kind = type.getKindForMimetype("vnd.android.cursor.item/xyz");
+        assertNotNull(kind);
+        assertEquals("data5", ((BaseAccountType.SimpleInflater) kind.actionHeader)
+                .getColumnNameForTest());
+        assertEquals("data6", ((BaseAccountType.SimpleInflater) kind.actionBody)
+                .getColumnNameForTest());
+        assertEquals(true, kind.actionBodySocial);
+    }
+
+    private static void assertsDataKindEquals(List<DataKind> expectedKinds,
+            List<DataKind> actualKinds) {
+        final int count = Math.max(actualKinds.size(), expectedKinds.size());
+        for (int i = 0; i < count; i++) {
+            String actual =  actualKinds.size() > i ? actualKinds.get(i).toString() : "(n/a)";
+            String expected =  expectedKinds.size() > i ? expectedKinds.get(i).toString() : "(n/a)";
+
+            // Because assertEquals()'s output is not very friendly when comparing two similar
+            // strings, we manually do the check.
+            if (!Objects.equal(actual, expected)) {
+                final int commonPrefixEnd = findCommonPrefixEnd(actual, expected);
+                fail("Kind #" + i
+                        + "\n[Actual]\n" + insertMarkerAt(actual, commonPrefixEnd)
+                        + "\n[Expected]\n" + insertMarkerAt(expected, commonPrefixEnd));
+            }
+        }
+    }
+
+    private static int findCommonPrefixEnd(String s1, String s2) {
+        int i = 0;
+        for (;;) {
+            final boolean s1End = (s1.length() <= i);
+            final boolean s2End = (s2.length() <= i);
+            if (s1End || s2End) {
+                return i;
+            }
+            if (s1.charAt(i) != s2.charAt(i)) {
+                return i;
+            }
+            i++;
+        }
+    }
+
+    private static String insertMarkerAt(String s, int position) {
+        final String MARKER = "***";
+        if (position > s.length()) {
+            return s + MARKER;
+        } else {
+            return new StringBuilder(s).insert(position, MARKER).toString();
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/common/tests/testauth/TestAuthenticationService.java b/tests/src/com/android/contacts/common/tests/testauth/TestAuthenticationService.java
new file mode 100644
index 0000000..93d1f4a
--- /dev/null
+++ b/tests/src/com/android/contacts/common/tests/testauth/TestAuthenticationService.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.common.tests.testauth;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+public abstract class TestAuthenticationService extends Service {
+
+    private TestAuthenticator mAuthenticator;
+
+    @Override
+    public void onCreate() {
+        Log.v(TestauthConstants.LOG_TAG, this + " Service started.");
+        mAuthenticator = new TestAuthenticator(this);
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.v(TestauthConstants.LOG_TAG, this + " Service stopped.");
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        Log.v(TestauthConstants.LOG_TAG, this + " getBinder() intent=" + intent);
+        return mAuthenticator.getIBinder();
+    }
+
+    public static class Basic extends TestAuthenticationService {
+    }
+}
diff --git a/tests/src/com/android/contacts/common/tests/testauth/TestAuthenticator.java b/tests/src/com/android/contacts/common/tests/testauth/TestAuthenticator.java
new file mode 100644
index 0000000..2f676c7
--- /dev/null
+++ b/tests/src/com/android/contacts/common/tests/testauth/TestAuthenticator.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.common.tests.testauth;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+/**
+ * Simple authenticator.  It has no "login" dialogs/activities.  When you add a new account, it'll
+ * just create a new account with a unique name.
+ */
+class TestAuthenticator extends AbstractAccountAuthenticator {
+    private static final String PASSWORD = "xxx"; // any string will do.
+
+    // To remember the last user-ID.
+    private static final String PREF_KEY_LAST_USER_ID = "TestAuthenticator.PREF_KEY_LAST_USER_ID";
+
+    private final Context mContext;
+
+    public TestAuthenticator(Context context) {
+        super(context);
+        mContext = context.getApplicationContext();
+    }
+
+    /**
+     * @return a new, unique username.
+     */
+    private String newUniqueUserName() {
+        final SharedPreferences prefs =
+                PreferenceManager.getDefaultSharedPreferences(mContext);
+        final int nextId = prefs.getInt(PREF_KEY_LAST_USER_ID, 0) + 1;
+        prefs.edit().putInt(PREF_KEY_LAST_USER_ID, nextId).apply();
+
+        return "User-" + nextId;
+    }
+
+    /**
+     * Create a new account with the name generated by {@link #newUniqueUserName()}.
+     */
+    @Override
+    public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
+            String authTokenType, String[] requiredFeatures, Bundle options) {
+        Log.v(TestauthConstants.LOG_TAG, "addAccount() type=" + accountType);
+        final Bundle bundle = new Bundle();
+
+        final Account account = new Account(newUniqueUserName(), accountType);
+
+        // Create an account.
+        AccountManager.get(mContext).addAccountExplicitly(account, PASSWORD, null);
+
+        // And return it.
+        bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+        bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
+        return bundle;
+    }
+
+    /**
+     * Just return the user name as the authtoken.
+     */
+    @Override
+    public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
+            String authTokenType, Bundle loginOptions) {
+        Log.v(TestauthConstants.LOG_TAG, "getAuthToken() account=" + account);
+        final Bundle bundle = new Bundle();
+        bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+        bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
+        bundle.putString(AccountManager.KEY_AUTHTOKEN, account.name);
+
+        return bundle;
+    }
+
+    @Override
+    public Bundle confirmCredentials(
+            AccountAuthenticatorResponse response, Account account, Bundle options) {
+        Log.v(TestauthConstants.LOG_TAG, "confirmCredentials()");
+        return null;
+    }
+
+    @Override
+    public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
+        Log.v(TestauthConstants.LOG_TAG, "editProperties()");
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getAuthTokenLabel(String authTokenType) {
+        // null means we don't support multiple authToken types
+        Log.v(TestauthConstants.LOG_TAG, "getAuthTokenLabel()");
+        return null;
+    }
+
+    @Override
+    public Bundle hasFeatures(
+            AccountAuthenticatorResponse response, Account account, String[] features) {
+        // This call is used to query whether the Authenticator supports
+        // specific features. We don't expect to get called, so we always
+        // return false (no) for any queries.
+        Log.v(TestauthConstants.LOG_TAG, "hasFeatures()");
+        final Bundle result = new Bundle();
+        result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
+        return result;
+    }
+
+    @Override
+    public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
+            String authTokenType, Bundle loginOptions) {
+        Log.v(TestauthConstants.LOG_TAG, "updateCredentials()");
+        return null;
+    }
+}
diff --git a/tests/src/com/android/contacts/common/tests/testauth/TestSyncAdapter.java b/tests/src/com/android/contacts/common/tests/testauth/TestSyncAdapter.java
new file mode 100644
index 0000000..a7c0f83
--- /dev/null
+++ b/tests/src/com/android/contacts/common/tests/testauth/TestSyncAdapter.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.contacts.common.tests.testauth;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SyncResult;
+import android.os.Bundle;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+/**
+ * Simple (minimal) sync adapter.
+ *
+ */
+public class TestSyncAdapter extends AbstractThreadedSyncAdapter {
+    private final AccountManager mAccountManager;
+
+    private final Context mContext;
+
+    public TestSyncAdapter(Context context, boolean autoInitialize) {
+        super(context, autoInitialize);
+        mContext = context.getApplicationContext();
+        mAccountManager = AccountManager.get(mContext);
+    }
+
+    /**
+     * Doesn't actually sync, but sweep up all existing local-only contacts.
+     */
+    @Override
+    public void onPerformSync(Account account, Bundle extras, String authority,
+            ContentProviderClient provider, SyncResult syncResult) {
+        Log.v(TestauthConstants.LOG_TAG, "TestSyncAdapter.onPerformSync() account=" + account);
+
+        // First, claim all local-only contacts, if any.
+        ContentResolver cr = mContext.getContentResolver();
+        ContentValues values = new ContentValues();
+        values.put(RawContacts.ACCOUNT_NAME, account.name);
+        values.put(RawContacts.ACCOUNT_TYPE, account.type);
+        final int count = cr.update(RawContacts.CONTENT_URI, values,
+                RawContacts.ACCOUNT_NAME + " IS NULL AND " + RawContacts.ACCOUNT_TYPE + " IS NULL",
+                null);
+        if (count > 0) {
+            Log.v(TestauthConstants.LOG_TAG, "Claimed " + count + " local raw contacts");
+        }
+
+        // TODO: Clear isDirty flag
+        // TODO: Remove isDeleted raw contacts
+    }
+}
diff --git a/tests/src/com/android/contacts/common/tests/testauth/TestSyncService.java b/tests/src/com/android/contacts/common/tests/testauth/TestSyncService.java
new file mode 100644
index 0000000..3354cb4
--- /dev/null
+++ b/tests/src/com/android/contacts/common/tests/testauth/TestSyncService.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.contacts.common.tests.testauth;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public abstract class TestSyncService extends Service {
+
+    private static TestSyncAdapter sSyncAdapter;
+
+    @Override
+    public void onCreate() {
+        if (sSyncAdapter == null) {
+            sSyncAdapter = new TestSyncAdapter(getApplicationContext(), true);
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return sSyncAdapter.getSyncAdapterBinder();
+    }
+
+    public static class Basic extends TestSyncService {
+    }
+}
diff --git a/tests/src/com/android/contacts/common/tests/testauth/TestauthConstants.java b/tests/src/com/android/contacts/common/tests/testauth/TestauthConstants.java
new file mode 100644
index 0000000..3ce7f5a
--- /dev/null
+++ b/tests/src/com/android/contacts/common/tests/testauth/TestauthConstants.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.common.tests.testauth;
+
+class TestauthConstants {
+    public static final String LOG_TAG = "Testauth";
+}